infiniti scroll product list

This commit is contained in:
hamid zarghami
2025-09-06 15:09:58 +03:30
parent 0f77a3208d
commit 5ec2b621bc
5 changed files with 122 additions and 19 deletions
+22
View File
@@ -26,6 +26,7 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.62.0",
"react-infinite-scroll-component": "^6.1.0",
"react-otp-input": "^3.1.1",
"react-spinners": "^0.17.0",
"swiper": "^11.2.10",
@@ -5809,6 +5810,18 @@
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-infinite-scroll-component": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz",
"integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==",
"license": "MIT",
"dependencies": {
"throttle-debounce": "^2.1.0"
},
"peerDependencies": {
"react": ">=16.0.0"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -6509,6 +6522,15 @@
"node": ">=18"
}
},
"node_modules/throttle-debounce": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz",
"integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/tiny-case": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
+1
View File
@@ -27,6 +27,7 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.62.0",
"react-infinite-scroll-component": "^6.1.0",
"react-otp-input": "^3.1.1",
"react-spinners": "^0.17.0",
"swiper": "^11.2.10",
+46 -19
View File
@@ -12,12 +12,12 @@ 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, useEffect } from 'react'
import { useCategoryProductsData } from '../hooks/useProductsData'
import { useState, useEffect, useMemo } from 'react'
import InfiniteScroll from 'react-infinite-scroll-component'
import { useInfiniteCategoryProductsData } from '../hooks/useInfiniteProductsData'
import { IProduct } from '@/types/landing.types'
import { Product } from '@/types/products.types'
import { useSearchParams, useParams } from 'next/navigation'
@@ -55,8 +55,20 @@ const Products: NextPage = () => {
setFilterParams(params)
}, [searchParams])
// دریافت داده‌های محصولات از API بر اساس categoryUrl
const { data, isLoading, error } = useCategoryProductsData(categoryUrl, filterParams)
// دریافت داده‌های محصولات از API بر اساس categoryUrl با infinite scroll
const {
data,
isLoading,
error,
fetchNextPage,
hasNextPage
} = useInfiniteCategoryProductsData(categoryUrl, 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 => {
@@ -91,6 +103,7 @@ const Products: NextPage = () => {
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',
@@ -117,11 +130,11 @@ const Products: NextPage = () => {
<BreadcrumbItem>
<BreadcrumbLink href="/">فروشگاه آناهیتا</BreadcrumbLink>
</BreadcrumbItem>
{data?.results.breadcrumb?.map((item, index) => (
{data?.pages?.[0]?.results.breadcrumb?.map((item, index) => (
<div key={item._id} className="flex items-center">
<BreadcrumbSeparator className="text-muted-foreground select-none">/</BreadcrumbSeparator>
<BreadcrumbItem>
{index === data.results.breadcrumb.length - 1 ? (
{index === (data?.pages?.[0]?.results.breadcrumb?.length || 0) - 1 ? (
<BreadcrumbPage className="text-foreground">
{item.title_fa}
</BreadcrumbPage>
@@ -160,7 +173,7 @@ const Products: NextPage = () => {
/>
<div className='flex-1'>
<Sorts data={data} />
<Sorts data={data?.pages?.[0]} />
<div className='mt-6 sm:mt-8 md:mt-10'>
{isLoading ? (
@@ -171,15 +184,31 @@ const Products: NextPage = () => {
<div className="text-center py-8">
<div className="text-red-500">خطا در بارگذاری محصولات</div>
</div>
) : data?.results.products ? (
<GridWrapper desktop={4} mobile={2} gapMobile={3} gapDesktop={6}>
{data.results.products.map((product) => (
<ProductCard
key={product._id}
item={convertToIProduct(product)}
/>
))}
</GridWrapper>
) : 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) => (
<ProductCard
key={product._id}
item={convertToIProduct(product)}
/>
))}
</GridWrapper>
</InfiniteScroll>
) : (
<div className="text-center py-8">
<div className="text-gray-500">محصولی یافت نشد</div>
@@ -187,8 +216,6 @@ const Products: NextPage = () => {
)}
</div>
{/* Pagination */}
<Pagination pager={data?.results?.pager} />
</div>
</div>
</div>
@@ -0,0 +1,50 @@
import { useInfiniteQuery } from "@tanstack/react-query";
import * as api from "../service/Service";
import { ProductsResponse } from "@/types/products.types";
interface UseInfiniteProductsDataParams {
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 useInfiniteProductsData = (
params?: UseInfiniteProductsDataParams
) => {
return useInfiniteQuery<ProductsResponse>({
queryKey: ["infinite-products", params],
queryFn: ({ pageParam = 1 }) =>
api.getProducts({ ...params, page: pageParam }),
getNextPageParam: (lastPage) => {
const pager = lastPage.results.pager;
return pager.nextPage ? pager.page + 1 : undefined;
},
initialPageParam: 1,
});
};
export const useInfiniteCategoryProductsData = (
categoryUrl: string,
params?: UseInfiniteProductsDataParams
) => {
return useInfiniteQuery<ProductsResponse>({
queryKey: ["infinite-category-products", categoryUrl, params],
queryFn: ({ pageParam = 1 }) =>
api.getCategoryProducts(categoryUrl, { ...params, page: pageParam }),
getNextPageParam: (lastPage) => {
const pager = lastPage.results.pager;
return pager.nextPage ? pager.page + 1 : undefined;
},
initialPageParam: 1,
});
};
+3
View File
@@ -4,6 +4,7 @@ import { Heart } from 'iconsax-react'
import Image from 'next/image'
import { FC, useState } from 'react'
import { useAddWishlist, useRemoveWishlist } from '@/app/products/hooks/useProductsData'
import { useSharedStore } from '@/share/store/sharedStore'
type Props = {
item?: IProduct
@@ -12,6 +13,7 @@ type Props = {
const ProductCard: FC<Props> = ({ item }) => {
const { isLogin } = useSharedStore()
const [isWished, setIsWished] = useState(item?.isWishlist)
const { mutate: addWishlist } = useAddWishlist()
@@ -19,6 +21,7 @@ const ProductCard: FC<Props> = ({ item }) => {
const handleWish = () => {
if (!isLogin) return
if (!item?._id || !item?.variants[0]?._id) return
if (isWished) {