diff --git a/package-lock.json b/package-lock.json
index ecd4ae3..9f5c222 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-separator": "^1.1.7",
+ "@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.3",
"@tanstack/react-query": "^5.84.1",
"@tanstack/react-query-devtools": "^5.85.5",
@@ -999,6 +1000,12 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
+ "license": "MIT"
+ },
"node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
@@ -1288,6 +1295,45 @@
}
}
},
+ "node_modules/@radix-ui/react-slider": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz",
+ "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "license": "MIT"
+ },
"node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
diff --git a/package.json b/package.json
index 179f473..5583af7 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-separator": "^1.1.7",
+ "@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.3",
"@tanstack/react-query": "^5.84.1",
"@tanstack/react-query-devtools": "^5.85.5",
diff --git a/src/app/products/[id]/page.tsx b/src/app/products/[id]/page.tsx
index adddb5b..ca54c9c 100644
--- a/src/app/products/[id]/page.tsx
+++ b/src/app/products/[id]/page.tsx
@@ -12,13 +12,103 @@ import {
} from '@/components/ui/breadcrumb'
import Filters from '../components/Filters'
import Sorts from '../components/Sorts'
+import Pagination from '../components/Pagination'
import GridWrapper from '@/components/GridWrapper'
import ProductCard from '@/components/ProductCard'
import { Setting5 } from 'iconsax-react'
-import { useState } from 'react'
+import { useState, useEffect } from 'react'
+import { useCategoryProductsData } from '../hooks/useProductsData'
+import { IProduct } from '@/types/landing.types'
+import { Product } from '@/types/products.types'
+import { useSearchParams, useParams } from 'next/navigation'
const Products: NextPage = () => {
const [showMobileFilters, setShowMobileFilters] = useState(false)
+ const searchParams = useSearchParams()
+ const params = useParams()
+ const [filterParams, setFilterParams] = useState({
+ page: 1,
+ limit: 20,
+ sort: '3'
+ })
+
+ // استخراج categoryUrl از URL
+ const categoryUrl = params.id as string
+
+ // خواندن پارامترها از URL
+ useEffect(() => {
+ const params = {
+ page: parseInt(searchParams.get('page') || '1'),
+ limit: parseInt(searchParams.get('limit') || '20'),
+ sort: searchParams.get('sort') || '3',
+ category: searchParams.get('category') || undefined,
+ brand: searchParams.get('brand') || undefined,
+ minPrice: searchParams.get('minPrice') ? Number(searchParams.get('minPrice')) : undefined,
+ maxPrice: searchParams.get('maxPrice') ? Number(searchParams.get('maxPrice')) : undefined,
+ stock: searchParams.get('stock') || undefined,
+ wholeSale: searchParams.get('wholeSale') || undefined,
+ rating: searchParams.get('rating') ? Number(searchParams.get('rating')) : undefined,
+ attributes: searchParams.get('attributes')?.split(',').filter(Boolean) || undefined,
+ sizes: searchParams.get('sizes')?.split(',').filter(Boolean).map(Number) || undefined,
+ meterages: searchParams.get('meterages')?.split(',').filter(Boolean).map(Number) || undefined,
+ }
+ setFilterParams(params)
+ }, [searchParams])
+
+ // دریافت دادههای محصولات از API بر اساس categoryUrl
+ const { data, isLoading, error } = useCategoryProductsData(categoryUrl, filterParams)
+
+ // تبدیل 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,
+ 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 (
@@ -27,14 +117,22 @@ const Products: NextPage = () => {
فروشگاه آناهیتا
-
/
-
- بهداشت خانگی
-
-
/
-
- مواد شوینده
-
+ {data?.results.breadcrumb?.map((item, index) => (
+
+ /
+
+ {index === data.results.breadcrumb.length - 1 ? (
+
+ {item.title_fa}
+
+ ) : (
+
+ {item.title_fa}
+
+ )}
+
+
+ ))}
@@ -51,26 +149,46 @@ const Products: NextPage = () => {
{/* Desktop Filters */}
-
+
{/* Mobile Filters Modal */}
setShowMobileFilters(false)}
+ categoryUrl={categoryUrl}
/>
-
+
-
-
-
-
-
-
+ {isLoading ? (
+
+ ) : error ? (
+
+
خطا در بارگذاری محصولات
+
+ ) : data?.results.products ? (
+
+ {data.results.products.map((product) => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+ {/* Pagination */}
+
diff --git a/src/app/products/components/CategoryTree.tsx b/src/app/products/components/CategoryTree.tsx
index a5dcbc8..41498c5 100644
--- a/src/app/products/components/CategoryTree.tsx
+++ b/src/app/products/components/CategoryTree.tsx
@@ -2,27 +2,53 @@
import { FC, useCallback, useMemo, useState } from 'react'
import { ArrowDown2 } from 'iconsax-react'
-import { Category } from '@/share/types/SharedTypes'
+import { Category } from '@/types/products.types'
+import { useRouter, usePathname, useSearchParams } from 'next/navigation'
type Props = {
categories: Category[]
selectedId: string | null
onSelect: (id: string | null) => void
+ categoryUrl?: string
}
-const CategoryTree: FC = ({ categories, selectedId, onSelect }) => {
+const CategoryTree: FC = ({ categories, selectedId, onSelect, categoryUrl = 'Organic-supermarket' }) => {
+ const router = useRouter()
const roots = useMemo(() => categories || [], [categories])
+ const findCategoryById = useCallback((cats: Category[], id: string): Category | null => {
+ for (const cat of cats) {
+ if (cat._id === id) return cat
+ if (cat.children) {
+ const found = findCategoryById(cat.children, id)
+ if (found) return found
+ }
+ }
+ return null
+ }, [])
+
+ const handleCategorySelect = useCallback((id: string | null) => {
+ onSelect(id)
+ // تغییر URL به دستهبندی انتخاب شده
+ if (id) {
+ const selectedCategory = findCategoryById(categories, id)
+ if (selectedCategory) {
+ // ساخت URL به فرمت products/Organic-supermarket?sort=1&page=1
+ const categorySlug = selectedCategory.title_en.replace(/\s+/g, '-')
+ const newUrl = `/products/${categorySlug}?sort=1&page=1`
+ router.push(newUrl)
+ }
+ } else {
+ // اگر هیچ دستهبندی انتخاب نشده، به دستهبندی فعلی برگرد
+ const newUrl = `/products/${categoryUrl}?sort=1&page=1`
+ router.push(newUrl)
+ }
+ }, [onSelect, router, categories, findCategoryById, categoryUrl])
+
return (
- onSelect(null)}
- />
{roots.map((cat) => (
-
+
))}
)
@@ -40,8 +66,11 @@ const Node: FC = ({ node, selectedId, onSelect, depth }) => {
const hasChildren = (node.children?.length ?? 0) > 0
const toggle = useCallback(() => {
- if (hasChildren) setOpen((p) => !p)
- else onSelect(node._id)
+ if (hasChildren) {
+ setOpen((p) => !p)
+ } else {
+ onSelect(node._id)
+ }
}, [hasChildren, node._id, onSelect])
return (
@@ -63,8 +92,8 @@ const Node: FC = ({ node, selectedId, onSelect, depth }) => {
selectedId={selectedId}
onSelect={onSelect}
depth={depth + 1}
- />)
- )}
+ />
+ ))}
)}
diff --git a/src/app/products/components/Filters.tsx b/src/app/products/components/Filters.tsx
index 64dfd0e..46ca3fd 100644
--- a/src/app/products/components/Filters.tsx
+++ b/src/app/products/components/Filters.tsx
@@ -1,93 +1,116 @@
"use client"
-import { FC, useMemo, useState, useEffect } from 'react'
+import { FC, useMemo, useState, useEffect, useCallback, useRef } from 'react'
import { Setting5 } from 'iconsax-react'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
-import Input from '@/components/Input'
import { Button } from '@/components/ui/button'
+import { Slider } from '@/components/ui/slider'
import { useQuery } from '@tanstack/react-query'
-import { getCategories } from '@/share/service/ShareService'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import CategoryTree from './CategoryTree'
-import { Category } from '@/share/types/SharedTypes'
import Modal from '@/components/Modal'
+import * as api from '../service/Service'
+import { Brand, Category } from '@/types/products.types'
type FiltersProps = {
isMobile?: boolean
isOpen?: boolean
onClose?: () => void
+ categoryUrl?: string
}
-const Filters: FC = ({ isMobile = false, isOpen = false, onClose }) => {
+const Filters: FC = ({ isMobile = false, isOpen = false, onClose, categoryUrl = 'Organic-supermarket' }) => {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const [selectedCategories, setSelectedCategories] = useState([])
- const [minPrice, setMinPrice] = useState('')
- const [maxPrice, setMaxPrice] = useState('')
+ const [priceRange, setPriceRange] = useState<[number, number]>([0, 1000000])
const [inStockOnly, setInStockOnly] = useState(false)
- const [rating, setRating] = useState(null)
+ // const [rating, setRating] = useState(null)
+ const [selectedBrand, setSelectedBrand] = useState('')
+ const [wholeSale, setWholeSale] = useState(false)
- const { data: categoriesData } = useQuery({
- queryKey: ['categories'],
- queryFn: getCategories,
- staleTime: 1000 * 60 * 10,
+ // ref برای نگهداری timeout ID
+ const timeoutRef = useRef(undefined)
+
+ // تابع اعمال فیلتر قیمت با debounce
+ const applyPriceFilter = useCallback((range: [number, number]) => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current)
+ }
+
+ timeoutRef.current = setTimeout(() => {
+ const params = new URLSearchParams(searchParams.toString())
+ if (range[0] > 0) params.set('minPrice', range[0].toString())
+ else params.delete('minPrice')
+ if (range[1] < 1000000) params.set('maxPrice', range[1].toString())
+ else params.delete('maxPrice')
+ router.push(`${pathname}?${params.toString()}`)
+ }, 1000)
+ }, [searchParams, pathname, router])
+
+ // دریافت brands و categories از API محصولات
+ const { data: productsData } = useQuery({
+ queryKey: ['products-filters', categoryUrl, searchParams.toString()],
+ queryFn: () => {
+ const params = {
+ page: 1,
+ limit: 1,
+ category: searchParams.get('category') || undefined,
+ brand: searchParams.get('brand') || undefined,
+ minPrice: searchParams.get('minPrice') ? Number(searchParams.get('minPrice')) : undefined,
+ maxPrice: searchParams.get('maxPrice') ? Number(searchParams.get('maxPrice')) : undefined,
+ stock: searchParams.get('stock') || undefined,
+ wholeSale: searchParams.get('wholeSale') || undefined,
+ rating: searchParams.get('rating') ? Number(searchParams.get('rating')) : undefined,
+ }
+ return api.getCategoryProducts(categoryUrl, params)
+ },
+ staleTime: 1000 * 60 * 5,
})
const categories: Category[] = useMemo(() => {
- return categoriesData?.results?.data ?? []
- }, [categoriesData])
+ return productsData?.results?.category?.children ?? []
+ }, [productsData])
+
+ const brands: Brand[] = useMemo(() => {
+ return productsData?.results?.brands ?? []
+ }, [productsData])
useEffect(() => {
const qsCategories = searchParams.get('category')?.split(',').filter(Boolean) ?? []
- const qsMin = searchParams.get('minPrice') ?? ''
- const qsMax = searchParams.get('maxPrice') ?? ''
- const qsStock = searchParams.get('inStock') === '1'
- const qsRating = searchParams.get('rating')
+ const qsMin = searchParams.get('minPrice') ? Number(searchParams.get('minPrice')) : 0
+ const qsMax = searchParams.get('maxPrice') ? Number(searchParams.get('maxPrice')) : 1000000
+ const qsStock = searchParams.get('stock') === '1'
+ const qsBrand = searchParams.get('brand') ?? ''
+ const qsWholeSale = searchParams.get('wholeSale') === '1'
setSelectedCategories(qsCategories)
- setMinPrice(qsMin)
- setMaxPrice(qsMax)
+ setPriceRange([qsMin, qsMax])
setInStockOnly(qsStock)
- setRating(qsRating ? Number(qsRating) : null)
+ setSelectedBrand(qsBrand)
+ setWholeSale(qsWholeSale)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
- const applyFilters = () => {
+ const clearFilters = () => {
+ setSelectedCategories([])
+ setPriceRange([0, 1000000])
+ setInStockOnly(false)
+ setSelectedBrand('')
+ setWholeSale(false)
const params = new URLSearchParams(searchParams.toString())
- if (selectedCategories.length > 0) {
- params.set('category', selectedCategories.join(','))
- } else {
- params.delete('category')
- }
- if (minPrice) params.set('minPrice', minPrice)
- else params.delete('minPrice')
- if (maxPrice) params.set('maxPrice', maxPrice)
- else params.delete('maxPrice')
- if (inStockOnly) params.set('inStock', '1')
- else params.delete('inStock')
- if (rating) params.set('rating', String(rating))
- else params.delete('rating')
+ ;['category', 'minPrice', 'maxPrice', 'stock', 'brand', 'wholeSale'].forEach((k) => params.delete(k))
router.push(`${pathname}?${params.toString()}`)
- // Close mobile modal after applying filters
+ // Close mobile modal after clearing filters
if (isMobile && onClose) {
onClose()
}
}
- const clearFilters = () => {
- setSelectedCategories([])
- setMinPrice('')
- setMaxPrice('')
- setInStockOnly(false)
- setRating(null)
- const params = new URLSearchParams(searchParams.toString())
- ;['category', 'minPrice', 'maxPrice', 'inStock', 'rating'].forEach((k) => params.delete(k))
- router.push(`${pathname}?${params.toString()}`)
- }
const FilterContent = () => (
<>
@@ -104,7 +127,19 @@ const Filters: FC = ({ isMobile = false, isOpen = false, onClose }
setSelectedCategories(id ? [id] : [])}
+ onSelect={(id) => {
+ const newCategories = id ? [id] : []
+ setSelectedCategories(newCategories)
+ // اعمال فیلتر دستهبندی بلافاصله
+ const params = new URLSearchParams(searchParams.toString())
+ if (newCategories.length > 0) {
+ params.set('category', newCategories.join(','))
+ } else {
+ params.delete('category')
+ }
+ router.push(`${pathname}?${params.toString()}`)
+ }}
+ categoryUrl={categoryUrl}
/>
@@ -112,23 +147,24 @@ const Filters: FC = ({ isMobile = false, isOpen = false, onClose }
قیمت
-
-
setMinPrice(e.target.value)}
- placeholder='0'
- seprator
- inputMode='numeric'
- />
-
setMaxPrice(e.target.value)}
- placeholder='0'
- seprator
- inputMode='numeric'
- />
+
+
+ {
+ setPriceRange(value as [number, number])
+ applyPriceFilter(value as [number, number])
+ }}
+ min={0}
+ max={1000000}
+ step={10000}
+ className='w-full'
+ />
+
+
+ حداقل: {priceRange[0].toLocaleString('fa-IR')} تومان
+ حداکثر: {priceRange[1].toLocaleString('fa-IR')} تومان
+
@@ -141,45 +177,92 @@ const Filters: FC
= ({ isMobile = false, isOpen = false, onClose }
type='checkbox'
className='size-4 rounded border-border'
checked={inStockOnly}
- onChange={(e) => setInStockOnly(e.target.checked)}
+ onChange={(e) => {
+ setInStockOnly(e.target.checked)
+ // اعمال فیلتر موجودی بلافاصله
+ const params = new URLSearchParams(searchParams.toString())
+ if (e.target.checked) {
+ params.set('stock', '1')
+ } else {
+ params.delete('stock')
+ }
+ router.push(`${pathname}?${params.toString()}`)
+ }}
/>
فقط کالاهای موجود
-
- امتیاز خریداران
+
+ برند
-
-
-
-
+
+
>
)
diff --git a/src/app/products/components/Pagination.tsx b/src/app/products/components/Pagination.tsx
new file mode 100644
index 0000000..af5c4d6
--- /dev/null
+++ b/src/app/products/components/Pagination.tsx
@@ -0,0 +1,116 @@
+'use client'
+
+import { FC } from 'react'
+import { useRouter, useSearchParams, usePathname } from 'next/navigation'
+import { Button } from '@/components/ui/button'
+import { ArrowLeft2, ArrowRight2 } from 'iconsax-react'
+import { Pager } from '@/types/products.types'
+
+type PaginationProps = {
+ pager?: Pager
+}
+
+const Pagination: FC
= ({ pager }) => {
+ const router = useRouter()
+ const pathname = usePathname()
+ const searchParams = useSearchParams()
+
+ if (!pager || pager.totalPages <= 1) {
+ return null
+ }
+
+ const handlePageChange = (page: number) => {
+ const params = new URLSearchParams(searchParams.toString())
+ params.set('page', page.toString())
+ router.push(`${pathname}?${params.toString()}`)
+ }
+
+ const generatePageNumbers = () => {
+ const pages = []
+ const currentPage = pager.page
+ const totalPages = pager.totalPages
+ const maxVisiblePages = 5
+
+ if (totalPages <= maxVisiblePages) {
+ for (let i = 1; i <= totalPages; i++) {
+ pages.push(i)
+ }
+ } else {
+ if (currentPage <= 3) {
+ for (let i = 1; i <= 4; i++) {
+ pages.push(i)
+ }
+ pages.push('...')
+ pages.push(totalPages)
+ } else if (currentPage >= totalPages - 2) {
+ pages.push(1)
+ pages.push('...')
+ for (let i = totalPages - 3; i <= totalPages; i++) {
+ pages.push(i)
+ }
+ } else {
+ pages.push(1)
+ pages.push('...')
+ for (let i = currentPage - 1; i <= currentPage + 1; i++) {
+ pages.push(i)
+ }
+ pages.push('...')
+ pages.push(totalPages)
+ }
+ }
+
+ return pages
+ }
+
+ const pageNumbers = generatePageNumbers()
+
+ return (
+
+ {/* Previous Button */}
+
+
+ {/* Page Numbers */}
+
+ {pageNumbers.map((page, index) => (
+
+ {page === '...' ? (
+ ...
+ ) : (
+
+ )}
+
+ ))}
+
+
+ {/* Next Button */}
+
+
+ )
+}
+
+export default Pagination
diff --git a/src/app/products/components/Sorts.tsx b/src/app/products/components/Sorts.tsx
index d9ff742..aa73052 100644
--- a/src/app/products/components/Sorts.tsx
+++ b/src/app/products/components/Sorts.tsx
@@ -1,23 +1,47 @@
import { Sort, ArrowDown2 } from 'iconsax-react'
import { FC, useState, useEffect, useRef } from 'react'
+import { useRouter, useSearchParams, usePathname } from 'next/navigation'
+import { ProductsResponse } from '@/types/products.types'
const sortOptions = [
- { key: 'relevant', label: 'مرتبط ترین' },
- { key: 'most_visited', label: 'پربازدیدترین' },
- { key: 'newest', label: 'جدیدترین' },
- { key: 'best_selling', label: 'پرفروش ترین' },
- { key: 'cheapest', label: 'ارزانترین' },
- { key: 'most_expensive', label: 'گرانترین' },
- { key: 'customer_suggestion', label: 'پیشنهاد خریداران' },
+ { key: '1', label: 'مرتبط ترین' },
+ { key: '2', label: 'پربازدیدترین' },
+ { key: '3', label: 'جدیدترین' },
+ { key: '4', label: 'پرفروش ترین' },
+ { key: '5', label: 'ارزانترین' },
+ { key: '6', label: 'گرانترین' },
+ { key: '7', label: 'پیشنهاد خریداران' },
]
-const Sorts: FC = () => {
- const [selectedSort, setSelectedSort] = useState('newest')
+type SortsProps = {
+ data?: ProductsResponse
+}
+
+const Sorts: 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)) {
@@ -47,7 +71,7 @@ const Sorts: FC = () => {
setSelectedSort(option.key)}
+ onClick={() => handleSortChange(option.key)}
>
{option.label}
@@ -74,7 +98,7 @@ const Sorts: FC = () => {
className={`px-4 py-3 cursor-pointer text-sm hover:bg-gray-50 ${selectedSort === option.key ? 'text-primary bg-blue-50' : 'text-[#333333]'
}`}
onClick={() => {
- setSelectedSort(option.key)
+ handleSortChange(option.key)
setShowDropdown(false)
}}
>
@@ -86,7 +110,7 @@ const Sorts: FC = () => {
- 100 کالا
+ {data?.results?.pager?.totalItems || 0} کالا
)
diff --git a/src/app/products/hooks/useProductsData.ts b/src/app/products/hooks/useProductsData.ts
new file mode 100644
index 0000000..3475be2
--- /dev/null
+++ b/src/app/products/hooks/useProductsData.ts
@@ -0,0 +1,36 @@
+import { useQuery } from "@tanstack/react-query";
+import * as api from "../service/Service";
+import { ProductsResponse } from "@/types/products.types";
+
+interface UseProductsDataParams {
+ page?: number;
+ limit?: number;
+ category?: string;
+ brand?: string;
+ minPrice?: number;
+ maxPrice?: number;
+ attributes?: string[];
+ sizes?: number[];
+ meterages?: number[];
+ sort?: string;
+ wholeSale?: string;
+ stock?: string;
+ rating?: number;
+}
+
+export const useProductsData = (params?: UseProductsDataParams) => {
+ return useQuery({
+ queryKey: ["products", params],
+ queryFn: () => api.getProducts(params),
+ });
+};
+
+export const useCategoryProductsData = (
+ categoryUrl: string,
+ params?: UseProductsDataParams
+) => {
+ return useQuery({
+ queryKey: ["category-products", categoryUrl, params],
+ queryFn: () => api.getCategoryProducts(categoryUrl, params),
+ });
+};
diff --git a/src/app/products/service/Service.ts b/src/app/products/service/Service.ts
new file mode 100644
index 0000000..a838717
--- /dev/null
+++ b/src/app/products/service/Service.ts
@@ -0,0 +1,47 @@
+import axios from "@/config/axios";
+import { ProductsResponse } from "@/types/products.types";
+
+export const getProducts = async (params?: {
+ page?: number;
+ limit?: number;
+ category?: string;
+ brand?: string;
+ minPrice?: number;
+ maxPrice?: number;
+ attributes?: string[];
+ sizes?: number[];
+ meterages?: number[];
+ sort?: string;
+ wholeSale?: string;
+ stock?: string;
+ rating?: number;
+}): Promise => {
+ const { data } = await axios.get("/category/Organic-supermarket/search", {
+ params,
+ });
+ return data;
+};
+
+export const getCategoryProducts = async (
+ categoryUrl: string,
+ params?: {
+ page?: number;
+ limit?: number;
+ brand?: string;
+ minPrice?: number;
+ maxPrice?: number;
+ attributes?: string[];
+ sizes?: number[];
+ meterages?: number[];
+ sort?: string;
+ wholeSale?: string;
+ stock?: string;
+ category?: string;
+ rating?: number;
+ }
+): Promise => {
+ const { data } = await axios.get(`/category/${categoryUrl}/search`, {
+ params,
+ });
+ return data;
+};
diff --git a/src/components/ProductCard.tsx b/src/components/ProductCard.tsx
index e00df26..740fd58 100644
--- a/src/components/ProductCard.tsx
+++ b/src/components/ProductCard.tsx
@@ -25,13 +25,37 @@ const ProductCard: FC = ({ item }) => {
{item.title_fa}
+ {/*
-
-
-
-
+ {
+ item.variants.map((variant) => {
+ if (variant.meterage) {
+ return
{variant?.meterage?.value}
+ }
+
+ })
+ }
+
+ {
+ item.variants.map((variant) => {
+ if (variant.color) {
+ return
+ }
+ })
+ }
+ {
+ item.variants.map((variant) => {
+ if (variant.size) {
+ return
{variant?.size?.value}
+ }
+ })
+ }
+
*/}
+
{NumberFormat(item.default_variant.price.selling_price)} تومان
diff --git a/src/components/ui/slider.tsx b/src/components/ui/slider.tsx
new file mode 100644
index 0000000..09391e8
--- /dev/null
+++ b/src/components/ui/slider.tsx
@@ -0,0 +1,63 @@
+"use client"
+
+import * as React from "react"
+import * as SliderPrimitive from "@radix-ui/react-slider"
+
+import { cn } from "@/lib/utils"
+
+function Slider({
+ className,
+ defaultValue,
+ value,
+ min = 0,
+ max = 100,
+ ...props
+}: React.ComponentProps
) {
+ const _values = React.useMemo(
+ () =>
+ Array.isArray(value)
+ ? value
+ : Array.isArray(defaultValue)
+ ? defaultValue
+ : [min, max],
+ [value, defaultValue, min, max]
+ )
+
+ return (
+
+
+
+
+ {Array.from({ length: _values.length }, (_, index) => (
+
+ ))}
+
+ )
+}
+
+export { Slider }
diff --git a/src/share/Footer.tsx b/src/share/Footer.tsx
index 9c28c03..0c4c047 100644
--- a/src/share/Footer.tsx
+++ b/src/share/Footer.tsx
@@ -39,7 +39,7 @@ const Footer: FC = () => {
-
+
دانلود اپلیکیشن
@@ -61,7 +61,7 @@ const Footer: FC = () => {
-
+
با ثبت ایمیل از آخرین تخفیف ها با خبر شوید
@@ -77,7 +77,7 @@ const Footer: FC = () => {
-
+
ما را در شبکه های اجتماعی دنبال کنید
diff --git a/src/types/landing.types.ts b/src/types/landing.types.ts
index 8947a8d..0b4de28 100644
--- a/src/types/landing.types.ts
+++ b/src/types/landing.types.ts
@@ -25,6 +25,42 @@ export interface IDefaultVariant {
isWholeSale: boolean;
}
+export interface IShipmentMethod {
+ _id: number;
+ name: string;
+ description: string;
+ deliveryTime: number;
+ deliveryType: "Standard" | "SameDay" | "Express";
+}
+
+export interface IWarranty {
+ _id: number;
+ duration: string;
+ logoUrl: string;
+ name: string;
+}
+
+export interface IMeterage {
+ _id: number;
+ value: string;
+}
+
+export interface IVariant {
+ _id: string;
+ market_status: "Marketable" | "Unmarketable" | "OutOfStock";
+ price: IPrice;
+ stock: number;
+ postingTime: number;
+ isFreeShip: boolean;
+ isWholeSale: boolean;
+ shop: IShop;
+ shipmentMethod: IShipmentMethod[];
+ warranty: IWarranty;
+ meterage?: IMeterage;
+ size?: IMeterage;
+ color?: IMeterage;
+}
+
export interface IImagesUrl {
cover: string;
list: string[];
@@ -44,6 +80,7 @@ export interface IProduct {
imagesUrl: IImagesUrl;
url: string;
default_variant: IDefaultVariant;
+ variants: IVariant[];
}
export interface ICategory {
diff --git a/src/types/products.types.ts b/src/types/products.types.ts
new file mode 100644
index 0000000..4a5fe81
--- /dev/null
+++ b/src/types/products.types.ts
@@ -0,0 +1,200 @@
+export interface Product {
+ _id: number;
+ url: string;
+ title_fa: string;
+ title_en: string;
+ seoTitle?: string;
+ seoDescription?: string;
+ source: string;
+ description: string;
+ metaDescription?: string;
+ tags: string[];
+ advantages: string[];
+ disAdvantages: string[];
+ imagesUrl: {
+ cover: string;
+ list: string[];
+ };
+ isFake: string;
+ isWished: boolean | null;
+ specifications: Array<{
+ title: string;
+ values: string[];
+ }>;
+ brand: {
+ _id: string;
+ status: string;
+ title_en: string;
+ title_fa: string;
+ images: string[];
+ logoUrl: string;
+ };
+ category: {
+ _id: string;
+ title_fa: string;
+ title_en: string;
+ icon: string;
+ imageUrl: string;
+ theme: string;
+ description: string;
+ leaf: boolean;
+ parent: string;
+ };
+ default_variant?: ProductVariant;
+ variants: ProductVariant[];
+}
+
+export interface ProductVariant {
+ _id: string;
+ market_status: string;
+ price: {
+ 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;
+ };
+ stock: number;
+ postingTime: number;
+ isFreeShip: boolean;
+ isWholeSale: boolean;
+ shop: {
+ _id: string;
+ shopName: string;
+ shopCode: number;
+ owner: string;
+ shopDescription: string;
+ telephoneNumber: string;
+ isChatActive: boolean;
+ shopPostalCode: string;
+ logo: string;
+ };
+ shipmentMethod: Array<{
+ _id: number;
+ name: string;
+ description: string;
+ deliveryTime: number;
+ deliveryType: string;
+ }>;
+ warranty: {
+ _id: number;
+ duration: string;
+ logoUrl: string;
+ name: string;
+ };
+ meterage?: {
+ _id: number;
+ value: string;
+ };
+ size?: {
+ _id: number;
+ value: string;
+ };
+}
+
+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;
+ deleted: boolean;
+ createdAt: string;
+ updatedAt: string;
+ theme?: string;
+ url: string;
+ children: Category[];
+}
+
+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 FilterAttribute {
+ _id: number;
+ title: string;
+ hint: string;
+ type: string;
+ multiple: boolean;
+ required: boolean;
+ values: Array<{
+ _id: number;
+ attribute: number;
+ text: string;
+ createdAt: string;
+ updatedAt: string;
+ __v: number;
+ }>;
+}
+
+export interface PriceRange {
+ minPrice: number;
+ maxPrice: number;
+}
+
+export interface Size {
+ _id: number;
+ value: string;
+}
+
+export interface Meterage {
+ _id: number;
+ value: string;
+}
+
+export interface Breadcrumb {
+ _id: string;
+ title_fa: string;
+ title_en: string;
+ icon: string;
+ imageUrl: string;
+ description: string;
+ leaf: boolean;
+ parent: string | null;
+}
+
+export interface Pager {
+ page: number;
+ limit: number;
+ totalItems: number;
+ totalPages: number;
+ prevPage: boolean;
+ nextPage: string | null;
+}
+
+export interface ProductsResponse {
+ status: number;
+ success: boolean;
+ results: {
+ category: Category;
+ brands: Brand[];
+ products: Product[];
+ filters: {
+ attributes: FilterAttribute[];
+ priceRange: PriceRange;
+ sizes: Size[];
+ meterages: Meterage[];
+ };
+ breadcrumb: Breadcrumb[];
+ pager: Pager;
+ };
+}