feat(admin): optimize tender list pagination
continuous-integration/drone/push Build is passing

This commit is contained in:
AmirReza Jamali
2026-07-11 15:46:19 +03:30
parent 624614f1a9
commit a762a14026
7 changed files with 136 additions and 92 deletions
+33 -12
View File
@@ -5,7 +5,6 @@ import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import IsVisible from "@/components/ui/IsVisible";
import ListHeader from "@/components/ui/ListHeader";
import ListWrapper from "@/components/ui/ListWrapper";
import Pagination from "@/components/ui/pagination";
import Status from "@/components/ui/Status";
import {
Table,
@@ -49,7 +48,8 @@ const TendersTable = () => {
navigateToTenderDetails,
navigateToTenderFeedback,
handlePaginationChange,
buildWindowedPageLabel,
hasNext,
hasPrevious,
getRowNumber,
formatDateTimeCell,
pagination,
@@ -204,16 +204,37 @@ const TendersTable = () => {
/>
</div>
<IsVisible condition={shouldShowPagination}>
<Pagination
key={`pagination-tenders-${absoluteCurrentPage}`}
currentPage={pagination.currentPage}
totalPages={pagination.totalPages}
onPageChange={handlePaginationChange}
pageRangeDisplayed={3}
marginPagesDisplayed={1}
pageLabelBuilder={buildWindowedPageLabel}
variant="tenders"
/>
<nav
className="mt-2 flex w-full items-center justify-center gap-3 text-sm"
aria-label="Tender pagination"
>
<button
type="button"
disabled={!hasPrevious}
onClick={() =>
handlePaginationChange({ selected: absoluteCurrentPage - 1 })
}
className="shadow-theme-xs rounded-xl border border-stroke/80 bg-white/70 px-4 py-2 font-semibold text-dark transition hover:border-primary/35 hover:text-primary disabled:cursor-not-allowed disabled:opacity-40 dark:border-dark-3 dark:bg-dark-2/70 dark:text-white"
>
Previous
</button>
<span
className="text-gray-600 dark:text-gray-300"
aria-current="page"
>
Page {pagination.currentPage + 1}
</span>
<button
type="button"
disabled={!hasNext}
onClick={() =>
handlePaginationChange({ selected: absoluteCurrentPage + 1 })
}
className="shadow-theme-xs rounded-xl border border-stroke/80 bg-white/70 px-4 py-2 font-semibold text-dark transition hover:border-primary/35 hover:text-primary disabled:cursor-not-allowed disabled:opacity-40 dark:border-dark-3 dark:bg-dark-2/70 dark:text-white"
>
Next
</button>
</nav>
</IsVisible>
</ListWrapper>
);
@@ -8,14 +8,20 @@ import { TCustomer } from "@/lib/api";
import {
buildQueryString,
deleteEmptyKeys,
getPaginatedRowNumber,
isDateRangeActive,
unixToDate,
} from "@/utils/shared";
import gsap from "gsap";
import { useIsMutating } from "@tanstack/react-query";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import { useForm } from "react-hook-form";
const COMMA_SEPARATED_FILTER_KEYS = new Set([
@@ -356,11 +362,10 @@ const useTenderListPresenter = () => {
useEffect(() => {
const newParams: Record<string, any> = {
...apiDefaultParams,
offset: 0,
limit: 10,
limit: 20,
};
for (const key of searchParams.keys()) {
if (key === "cpv_codes") {
if (key === "cpv_codes" || key === "include_total" || key === "cursor") {
continue;
}
newParams[key] = searchParams.get(key);
@@ -376,53 +381,21 @@ const useTenderListPresenter = () => {
const { data, error, isPending, isError } = useTendersQuery(params);
const { pagination, handlePaginationChange, buildFirstPageParams } =
useHybridPagination({
meta: data?.meta,
params,
setParams,
isError,
error,
});
const {
pagination,
handlePaginationChange,
buildFirstPageParams,
hasNext,
hasPrevious,
} = useHybridPagination({
meta: data?.meta,
params,
setParams,
isError,
error,
});
const MAX_PAGE_JUMP = 10;
const paginationWindow = useMemo(() => {
const windowStart = Math.max(0, pagination.currentPage - MAX_PAGE_JUMP);
const windowEnd = Math.min(
Math.max(pagination.totalPages - 1, 0),
pagination.currentPage + MAX_PAGE_JUMP,
);
return {
windowStart,
virtualCurrent: pagination.currentPage - windowStart,
virtualTotal: Math.max(windowEnd - windowStart + 1, 1),
};
}, [pagination.currentPage, pagination.totalPages]);
const handleWindowedPaginationChange = useCallback(
(e: { selected: number }) => {
handlePaginationChange({
selected: e.selected + paginationWindow.windowStart,
});
},
[handlePaginationChange, paginationWindow.windowStart],
);
const buildWindowedPageLabel = useCallback(
(virtualPage: number) => String(virtualPage + paginationWindow.windowStart),
[paginationWindow.windowStart],
);
const windowedPagination = useMemo(
() => ({
currentPage: paginationWindow.virtualCurrent,
totalPages: paginationWindow.virtualTotal,
}),
[paginationWindow.virtualCurrent, paginationWindow.virtualTotal],
);
const shouldShowPagination =
!isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
const shouldShowPagination = !isPending && (hasPrevious || hasNext);
const tendersList = data?.data?.tenders ?? [];
@@ -449,12 +422,11 @@ const useTenderListPresenter = () => {
const getRowNumber = useCallback(
(index: number) =>
getPaginatedRowNumber({
index,
page: data?.meta?.page,
limit: data?.meta?.limit,
}),
[data?.meta?.limit, data?.meta?.page],
pagination.currentPage *
(data?.meta?.limit ?? Number(params.limit) ?? 20) +
index +
1,
[data?.meta?.limit, pagination.currentPage, params.limit],
);
const formatDateTimeCell = useCallback((unix: number) => {
@@ -541,7 +513,7 @@ const useTenderListPresenter = () => {
const clearAllFilters = useCallback(() => {
const cleared = buildFirstPageParams({
...apiDefaultParams,
limit: 10,
limit: 20,
});
filterFormReset(mergeTenderFilterFormState(cleared));
router.push(pathName);
@@ -629,11 +601,12 @@ const useTenderListPresenter = () => {
tendersList,
navigateToTenderDetails,
navigateToTenderFeedback,
handlePaginationChange: handleWindowedPaginationChange,
buildWindowedPageLabel,
handlePaginationChange,
hasNext,
hasPrevious,
getRowNumber,
formatDateTimeCell,
pagination: windowedPagination,
pagination,
absoluteCurrentPage: pagination.currentPage,
};
};