From a9ebbf6a4a5a6349c3f17fbcb747e85527409222 Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Fri, 13 Mar 2026 10:28:50 +0800 Subject: [PATCH] feat(pagination): add pagination PageNumber and InfiniteScroll components --- src/components/InfiniteScroll.tsx | 52 ++++++++++++++++++++ src/components/Pagination.tsx | 80 +++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 src/components/InfiniteScroll.tsx create mode 100644 src/components/Pagination.tsx diff --git a/src/components/InfiniteScroll.tsx b/src/components/InfiniteScroll.tsx new file mode 100644 index 0000000..08414bc --- /dev/null +++ b/src/components/InfiniteScroll.tsx @@ -0,0 +1,52 @@ +import React, { useEffect, useRef } from "react"; + +interface InfiniteScrollProps { + children: React.ReactNode; + onLoadMore: () => void; + hasMore: boolean; + isLoading: boolean; + className?: string; + loader?: React.ReactNode; +} + +export const InfiniteScroll: React.FC = ({ + children, + onLoadMore, + hasMore, + isLoading, + className = "", + loader, +}) => { + const observerTarget = useRef(null); + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMore && !isLoading) { + onLoadMore(); + } + }, + { threshold: 0.1 } + ); + + if (observerTarget.current) { + observer.observe(observerTarget.current); + } + + return () => observer.disconnect(); + }, [hasMore, isLoading, onLoadMore]); + + return ( +
+ {children} + {hasMore &&
} + {isLoading && ( + loader || ( +
+ Loading... +
+ ) + )} +
+ ); +}; diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx new file mode 100644 index 0000000..1399895 --- /dev/null +++ b/src/components/Pagination.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { useTranslation } from '../hooks/useTranslation'; + +interface PaginationProps { + currentPage: number; + totalCount: number; + limit: number; + onPageChange: (page: number) => void; + onLimitChange: (limit: number) => void; + pageSizeOptions?: number[]; +} + +export const Pagination: React.FC = ({ + currentPage, + totalCount, + limit, + onPageChange, + onLimitChange, + pageSizeOptions = [10, 20, 50], +}) => { + const { t, lang } = useTranslation(); + const isFa = lang === 'fa'; + + const toPersianNum = (num: string | number | undefined | null) => { + if (num === null || num === undefined) return num; + if (!isFa) return num; + return num.toString().replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[d as any]); + }; + + const totalPages = Math.ceil(totalCount / limit) || 1; + + if (totalCount === 0) return null; + + const startItem = ((currentPage - 1) * limit) + 1; + const endItem = Math.min(currentPage * limit, totalCount); + + return ( +
+
+ + + {t.pagination?.showing || 'Showing'} {toPersianNum(startItem)} {t.pagination?.to || '-'} {toPersianNum(endItem)} {t.pagination?.of || 'of'} {toPersianNum(totalCount)} + +
+ +
+ + + {t.pagination?.page || 'Page'} {toPersianNum(currentPage)} {t.pagination?.of || 'of'} {toPersianNum(totalPages)} + + +
+
+ ); +};