diff --git a/package-lock.json b/package-lock.json index 9f5c222..6bf54bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 5583af7..74d3a06 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/products/[id]/page.tsx b/src/app/products/[id]/page.tsx index ca54c9c..fd226ad 100644 --- a/src/app/products/[id]/page.tsx +++ b/src/app/products/[id]/page.tsx @@ -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 = () => { فروشگاه آناهیتا - {data?.results.breadcrumb?.map((item, index) => ( + {data?.pages?.[0]?.results.breadcrumb?.map((item, index) => (
/ - {index === data.results.breadcrumb.length - 1 ? ( + {index === (data?.pages?.[0]?.results.breadcrumb?.length || 0) - 1 ? ( {item.title_fa} @@ -160,7 +173,7 @@ const Products: NextPage = () => { />
- +
{isLoading ? ( @@ -171,15 +184,31 @@ const Products: NextPage = () => {
خطا در بارگذاری محصولات
- ) : data?.results.products ? ( - - {data.results.products.map((product) => ( - - ))} - + ) : allProducts.length > 0 ? ( + +
در حال بارگذاری محصولات بیشتر...
+
+ } + endMessage={ +
+ {/*
تمام محصولات بارگذاری شد
*/} +
+ } + > + + {allProducts.map((product) => ( + + ))} + + ) : (
محصولی یافت نشد
@@ -187,8 +216,6 @@ const Products: NextPage = () => { )}
- {/* Pagination */} -
diff --git a/src/app/products/hooks/useInfiniteProductsData.ts b/src/app/products/hooks/useInfiniteProductsData.ts new file mode 100644 index 0000000..fbfb97f --- /dev/null +++ b/src/app/products/hooks/useInfiniteProductsData.ts @@ -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({ + 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({ + 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, + }); +}; diff --git a/src/components/ProductCard.tsx b/src/components/ProductCard.tsx index a7c9ab1..c35dd65 100644 --- a/src/components/ProductCard.tsx +++ b/src/components/ProductCard.tsx @@ -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 = ({ item }) => { + const { isLogin } = useSharedStore() const [isWished, setIsWished] = useState(item?.isWishlist) const { mutate: addWishlist } = useAddWishlist() @@ -19,6 +21,7 @@ const ProductCard: FC = ({ item }) => { const handleWish = () => { + if (!isLogin) return if (!item?._id || !item?.variants[0]?._id) return if (isWished) {