brand page
This commit is contained in:
@@ -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 (
|
||||
<div className="mt-1.5">
|
||||
{/* معرفی برند */}
|
||||
{data?.pages?.[0]?.results.brand && (
|
||||
<BrandIntro brand={data.pages[0].results.brand} />
|
||||
)}
|
||||
|
||||
<div className=' px-4 sm:px-6 md:px-8 lg:px-20'>
|
||||
<Breadcrumb aria-label="breadcrumb">
|
||||
<BreadcrumbList className="text-sm text-muted-foreground">
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="/">فروشگاه آناهیتا</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
{data?.pages?.[0]?.results.brand && (
|
||||
<>
|
||||
<BreadcrumbSeparator className="text-muted-foreground select-none">/</BreadcrumbSeparator>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage className="text-foreground">
|
||||
{data.pages[0].results.brand.title_fa}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</>
|
||||
)}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
|
||||
{/* Mobile Filter Button */}
|
||||
<div className='lg:hidden mt-4 flex justify-between items-center'>
|
||||
<button
|
||||
onClick={() => setShowMobileFilters(true)}
|
||||
className='flex items-center gap-2 px-3 py-2 border border-border rounded-lg bg-white shadow-sm'
|
||||
>
|
||||
<Setting5 size={18} color='#666666' />
|
||||
<span className='text-sm text-[#666666]'>فیلترها</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='mt-6 sm:mt-8 md:mt-12 lg:mt-8 flex flex-col lg:flex-row gap-4 sm:gap-6'>
|
||||
{/* Desktop Filters */}
|
||||
<BrandFilters brandId={brandId} />
|
||||
|
||||
{/* Mobile Filters Modal */}
|
||||
<BrandFilters
|
||||
isMobile={true}
|
||||
isOpen={showMobileFilters}
|
||||
onClose={() => setShowMobileFilters(false)}
|
||||
brandId={brandId}
|
||||
/>
|
||||
|
||||
<div className='flex-1'>
|
||||
<BrandSorts data={data?.pages?.[0]} />
|
||||
|
||||
<div className='mt-6 sm:mt-8 md:mt-10'>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="text-gray-500">در حال بارگذاری...</div>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="text-red-500">خطا در بارگذاری محصولات</div>
|
||||
</div>
|
||||
) : allProducts.length > 0 ? (
|
||||
<InfiniteScroll
|
||||
dataLength={allProducts.length}
|
||||
next={fetchNextPage}
|
||||
hasMore={!!hasNextPage}
|
||||
loader={
|
||||
<div className="text-center py-4">
|
||||
<div className="text-gray-500">در حال بارگذاری محصولات بیشتر...</div>
|
||||
</div>
|
||||
}
|
||||
endMessage={
|
||||
<div className="text-center py-4">
|
||||
{/* <div className="text-gray-500">تمام محصولات بارگذاری شد</div> */}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<GridWrapper desktop={4} mobile={2} gapMobile={3} gapDesktop={6}>
|
||||
{allProducts.map((product: Product) => (
|
||||
<ProductCard
|
||||
key={product._id}
|
||||
item={convertToIProduct(product)}
|
||||
/>
|
||||
))}
|
||||
</GridWrapper>
|
||||
</InfiniteScroll>
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<div className="text-gray-500">محصولی یافت نشد</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Breadcrumb */}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function BrandPageWithLayout() {
|
||||
return (
|
||||
<Layout>
|
||||
<BrandPage />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
@@ -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<BrandFilterSectionProps> = ({
|
||||
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 (
|
||||
<div className='space-y-6'>
|
||||
{/* دستهبندیها */}
|
||||
<Accordion type='single' collapsible className='w-full'>
|
||||
<AccordionItem value='categories'>
|
||||
<AccordionTrigger className='text-sm font-medium'>
|
||||
دستهبندیها
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className='mt-4'>
|
||||
<CategoryTree
|
||||
categories={categories as any}
|
||||
selectedId={filterState.categories[0] || null}
|
||||
onSelect={(id) => id && onCategoryChange(id)}
|
||||
/>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* محدوده قیمت */}
|
||||
<AccordionItem value='price'>
|
||||
<AccordionTrigger className='text-sm font-medium'>
|
||||
محدوده قیمت
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className='mt-4 space-y-4'>
|
||||
{/* ورودیهای قیمت */}
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
<div>
|
||||
<label className='block text-xs text-gray-600 mb-1'>از</label>
|
||||
<Input
|
||||
type='text'
|
||||
value={NumberFormat(priceInputs.min)}
|
||||
onChange={(e) => handlePriceInputChange('min', e.target.value)}
|
||||
className='text-sm'
|
||||
placeholder='0'
|
||||
seprator={true}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className='block text-xs text-gray-600 mb-1'>تا</label>
|
||||
<Input
|
||||
type='text'
|
||||
value={NumberFormat(priceInputs.max)}
|
||||
onChange={(e) => handlePriceInputChange('max', e.target.value)}
|
||||
className='text-sm'
|
||||
placeholder='8,000,000'
|
||||
seprator={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* اسلایدر قیمت */}
|
||||
<div className='px-2'>
|
||||
<Slider
|
||||
value={[priceInputs.min, priceInputs.max]}
|
||||
onValueChange={(value) => {
|
||||
setPriceInputs({ min: value[0], max: value[1] })
|
||||
onPriceChange([value[0], value[1]])
|
||||
}}
|
||||
min={priceMinMaxRange.minPrice}
|
||||
max={priceMinMaxRange.maxPrice}
|
||||
step={100000}
|
||||
className='w-full'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* واحد قیمت */}
|
||||
<div className='text-center'>
|
||||
<span className='text-xs text-gray-500'>تومان</span>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default BrandFilterSection
|
||||
@@ -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<BrandFiltersProps> = ({
|
||||
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 = () => (
|
||||
<>
|
||||
<div className='flex items-center gap-2.5'>
|
||||
<Setting5 size={24} color='#666666' />
|
||||
<div className='text-[#666666]'>فیلترها</div>
|
||||
</div>
|
||||
|
||||
<BrandFilterSection
|
||||
categories={categories}
|
||||
filterState={filterState}
|
||||
onCategoryChange={applyCategoryFilter}
|
||||
onPriceChange={updatePriceRange}
|
||||
brandId={brandId}
|
||||
priceMinMaxRange={priceMinMaxRange}
|
||||
/>
|
||||
|
||||
<div className='mt-4'>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={handleClearFilters}
|
||||
className='w-full'
|
||||
disabled={isLoading}
|
||||
>
|
||||
حذف همه فیلترها
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Modal
|
||||
open={isOpen}
|
||||
close={onClose || (() => { })}
|
||||
title="فیلترها"
|
||||
isHeader={true}
|
||||
>
|
||||
<div className='max-h-[70vh] overflow-y-auto'>
|
||||
<FilterContent />
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-[270px] hidden lg:block border border-border rounded-[10px] p-5 h-fit'>
|
||||
<FilterContent />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BrandFilters
|
||||
@@ -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<Props> = ({ brand }) => {
|
||||
return (
|
||||
<div className='bg-[#FAFAFA] rounded-2xl p-6 px-24 mb-8 flex justify-between'>
|
||||
<div className='flex gap-10'>
|
||||
<div className='bg-[#F2F2F2] size-[118px] rounded-xl flex items-center justify-center'>
|
||||
<Image
|
||||
src={brand.logoUrl}
|
||||
width={112}
|
||||
height={112}
|
||||
alt={brand.title_fa}
|
||||
className='object-contain max-w-[100px] max-h-[100px]'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1 className='text-[#333333] text-xl '>
|
||||
{brand.title_en?.toUpperCase()}
|
||||
</h1>
|
||||
<h4 className='text-[#999999] text-sm mt-2'>
|
||||
{brand.title_fa}
|
||||
</h4>
|
||||
|
||||
<p className='mt-7 text-[#333333] text-sm'>
|
||||
{brand.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BrandIntro
|
||||
@@ -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<Props> = ({ products, brandName }) => {
|
||||
return (
|
||||
<div className='bg-white rounded-2xl p-6 shadow-sm'>
|
||||
<div className='flex items-center justify-between mb-6'>
|
||||
<h3 className='text-xl font-bold text-gray-800'>محصولات برند {brandName}</h3>
|
||||
<div className='flex items-center gap-2 text-sm text-gray-500'>
|
||||
<span>نمایش محصولات</span>
|
||||
<div className='w-2 h-2 bg-blue-500 rounded-full'></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Swiper
|
||||
modules={[Pagination, Navigation]}
|
||||
spaceBetween={16}
|
||||
slidesPerView='auto'
|
||||
pagination={{
|
||||
clickable: true,
|
||||
dynamicBullets: true,
|
||||
}}
|
||||
navigation={{
|
||||
nextEl: '.swiper-button-next',
|
||||
prevEl: '.swiper-button-prev',
|
||||
}}
|
||||
breakpoints={{
|
||||
640: {
|
||||
spaceBetween: 20,
|
||||
},
|
||||
768: {
|
||||
spaceBetween: 24,
|
||||
},
|
||||
}}
|
||||
className='brand-products-swiper'
|
||||
>
|
||||
{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 (
|
||||
<SwiperSlide key={product._id} className='!w-auto'>
|
||||
<div className='w-[240px] sm:w-[280px]'>
|
||||
<ProductCard item={iProduct} />
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
);
|
||||
})}
|
||||
</Swiper>
|
||||
|
||||
<style jsx global>{`
|
||||
.brand-products-swiper .swiper-pagination {
|
||||
position: relative;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.brand-products-swiper .swiper-pagination-bullet {
|
||||
background: #d1d5db;
|
||||
opacity: 1;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.brand-products-swiper .swiper-pagination-bullet-active {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.brand-products-swiper .swiper-button-next,
|
||||
.brand-products-swiper .swiper-button-prev {
|
||||
color: #3b82f6;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-top: -16px;
|
||||
}
|
||||
|
||||
.brand-products-swiper .swiper-button-next:after,
|
||||
.brand-products-swiper .swiper-button-prev:after {
|
||||
font-size: 16px;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BrandProductsCarousel
|
||||
@@ -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<BrandSortsProps> = ({ 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<HTMLDivElement>(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 (
|
||||
<div className='flex justify-between items-center text-[#999999] text-sm border-b border-border pb-5'>
|
||||
{/* Desktop Version */}
|
||||
<div className='hidden lg:flex items-center gap-4'>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Sort size={24} color='#333333' />
|
||||
<div className='text-sm text-[#333333]'>مرتب سازی:</div>
|
||||
</div>
|
||||
|
||||
{sortOptions.map((option) => (
|
||||
<div
|
||||
key={option.key}
|
||||
className={`cursor-pointer ${selectedSort === option.key ? 'text-primary' : ''}`}
|
||||
onClick={() => handleSortChange(option.key)}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Mobile Version */}
|
||||
<div className='lg:hidden flex items-center gap-2 relative' ref={dropdownRef}>
|
||||
<Sort size={20} color='#333333' />
|
||||
<div className='text-sm text-[#333333]'>مرتب سازی:</div>
|
||||
<div
|
||||
className='flex items-center gap-1 cursor-pointer bg-gray-50 px-3 py-1.5 rounded-lg'
|
||||
onClick={() => setShowDropdown(!showDropdown)}
|
||||
>
|
||||
<span className='text-sm text-[#333333]'>{selectedOption?.label}</span>
|
||||
<ArrowDown2 size={16} color='#333333' />
|
||||
</div>
|
||||
|
||||
{showDropdown && (
|
||||
<div className='absolute top-full right-0 mt-2 bg-white border border-border rounded-lg shadow-lg z-10 min-w-[180px]'>
|
||||
{sortOptions.map((option) => (
|
||||
<div
|
||||
key={option.key}
|
||||
className={`px-4 py-3 cursor-pointer text-sm hover:bg-gray-50 ${selectedSort === option.key ? 'text-primary bg-blue-50' : 'text-[#333333]'
|
||||
}`}
|
||||
onClick={() => {
|
||||
handleSortChange(option.key)
|
||||
setShowDropdown(false)
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='text-sm'>
|
||||
{data?.results?.pager?.totalItems || 0} کالا
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BrandSorts
|
||||
@@ -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<Props> = ({ 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 (
|
||||
<div className='flex items-center justify-center gap-2 mt-8'>
|
||||
{/* دکمه قبلی */}
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className='flex items-center justify-center w-10 h-10 rounded-lg border border-gray-300 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors'
|
||||
>
|
||||
<ArrowRight2 size={16} />
|
||||
</button>
|
||||
|
||||
{/* شماره صفحات */}
|
||||
<div className='flex items-center gap-2'>
|
||||
{visiblePages.map((page, index) => (
|
||||
<div key={index}>
|
||||
{page === '...' ? (
|
||||
<span className='px-3 py-2 text-gray-500'>...</span>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => onPageChange(page as number)}
|
||||
className={`w-10 h-10 rounded-lg text-sm font-medium transition-colors ${currentPage === page
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'border border-gray-300 text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* دکمه بعدی */}
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
className='flex items-center justify-center w-10 h-10 rounded-lg border border-gray-300 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors'
|
||||
>
|
||||
<ArrowLeft2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Pagination
|
||||
@@ -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<Props> = ({
|
||||
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 (
|
||||
<div className='bg-white rounded-2xl p-6 shadow-sm sticky top-4'>
|
||||
<h3 className='text-lg font-bold text-gray-800 mb-6'>فیلترها</h3>
|
||||
|
||||
{/* دستهبندیها */}
|
||||
<Accordion type='single' collapsible className='w-full'>
|
||||
<AccordionItem value='categories'>
|
||||
<AccordionTrigger className='text-sm font-medium'>
|
||||
دستهبندیها
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className='space-y-3 mt-4'>
|
||||
{categories.map((category) => (
|
||||
<label
|
||||
key={category._id}
|
||||
className='flex items-center gap-3 cursor-pointer hover:bg-gray-50 p-2 rounded-lg transition-colors'
|
||||
>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={selectedCategories.includes(category._id)}
|
||||
onChange={() => onCategoryChange(category._id)}
|
||||
className='w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500'
|
||||
/>
|
||||
<span className='text-sm text-gray-700 flex-1'>{category.title_fa}</span>
|
||||
<span className='text-xs text-gray-500'>({category.variants.length})</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* محدوده قیمت */}
|
||||
<AccordionItem value='price'>
|
||||
<AccordionTrigger className='text-sm font-medium'>
|
||||
محدوده قیمت
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className='mt-4 space-y-4'>
|
||||
{/* ورودیهای قیمت */}
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
<div>
|
||||
<label className='block text-xs text-gray-600 mb-1'>از</label>
|
||||
<Input
|
||||
type='text'
|
||||
value={NumberFormat(priceInputs.min)}
|
||||
onChange={(e) => handlePriceInputChange('min', e.target.value)}
|
||||
className='text-sm'
|
||||
placeholder='0'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className='block text-xs text-gray-600 mb-1'>تا</label>
|
||||
<Input
|
||||
type='text'
|
||||
value={NumberFormat(priceInputs.max)}
|
||||
onChange={(e) => handlePriceInputChange('max', e.target.value)}
|
||||
className='text-sm'
|
||||
placeholder='8,000,000'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* اسلایدر قیمت */}
|
||||
<div className='px-2'>
|
||||
<Slider
|
||||
value={selectedPriceRange}
|
||||
onValueChange={onPriceRangeChange}
|
||||
min={priceRange.minPrice}
|
||||
max={priceRange.maxPrice}
|
||||
step={100000}
|
||||
className='w-full'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* واحد قیمت */}
|
||||
<div className='text-center'>
|
||||
<span className='text-xs text-gray-500'>تومان</span>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductFilters
|
||||
@@ -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<Props> = ({ products, isLoading = false }) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='space-y-6'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<h2 className='text-xl font-bold text-gray-800'>محصولات تولید کننده</h2>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6'>
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<div key={index} className='animate-pulse'>
|
||||
<div className='bg-gray-200 rounded-2xl p-6 h-[320px]'></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='space-y-6'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<h2 className='text-xl font-bold text-gray-800'>محصولات تولید کننده</h2>
|
||||
<div className='text-sm text-gray-500'>
|
||||
{products.length} محصول یافت شد
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{products.length === 0 ? (
|
||||
<div className='text-center py-12'>
|
||||
<div className='text-gray-400 text-lg mb-2'>هیچ محصولی یافت نشد</div>
|
||||
<div className='text-gray-500 text-sm'>لطفاً فیلترهای خود را تغییر دهید</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6'>
|
||||
{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 (
|
||||
<ProductCard
|
||||
key={product._id}
|
||||
item={iProduct}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductGrid
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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<BrandResponse>({
|
||||
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,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import axios from "@/config/axios";
|
||||
import { BrandResponse, BrandProductsParams } from "../types/Types";
|
||||
|
||||
export const getBrandProducts = async (
|
||||
params: BrandProductsParams
|
||||
): Promise<BrandResponse> => {
|
||||
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<BrandResponse>(
|
||||
`/brand/${brandId}/search?${queryParams.toString()}`
|
||||
);
|
||||
return data;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user