diff --git a/src/components/Tables/tenders/index.tsx b/src/components/Tables/tenders/index.tsx
index c29b5e5..d94f174 100644
--- a/src/components/Tables/tenders/index.tsx
+++ b/src/components/Tables/tenders/index.tsx
@@ -46,6 +46,7 @@ const TendersTable = () => {
getRowNumber,
formatDateTimeCell,
pagination,
+ absoluteCurrentPage,
} = useTenderListPresenter();
return (
@@ -155,12 +156,14 @@ const TendersTable = () => {
diff --git a/src/components/Tables/tenders/useTenderListPresenter.ts b/src/components/Tables/tenders/useTenderListPresenter.ts
index 8143707..1a3d998 100644
--- a/src/components/Tables/tenders/useTenderListPresenter.ts
+++ b/src/components/Tables/tenders/useTenderListPresenter.ts
@@ -539,6 +539,7 @@ const useTenderListPresenter = () => {
getRowNumber,
formatDateTimeCell,
pagination: windowedPagination,
+ absoluteCurrentPage: pagination.currentPage,
};
};
diff --git a/src/components/ui/pagination.tsx b/src/components/ui/pagination.tsx
index e63fd6a..9227c0a 100644
--- a/src/components/ui/pagination.tsx
+++ b/src/components/ui/pagination.tsx
@@ -1,5 +1,8 @@
-import { FC, Fragment } from "react";
+import { FC } from "react";
import ReactPaginate, { ReactPaginateProps } from "react-paginate";
+
+type PaginationVariant = "default" | "tenders";
+
interface IProps {
currentPage: number;
totalPages: number;
@@ -7,8 +10,21 @@ interface IProps {
pageRangeDisplayed?: number;
marginPagesDisplayed?: number;
pageLabelBuilder?: ReactPaginateProps["pageLabelBuilder"];
+ variant?: PaginationVariant;
}
+const activeClassNames: Record = {
+ default:
+ "flex h-9 min-w-9 items-center justify-center rounded-xl border border-white/35 bg-gradient-to-br from-primary/85 via-primary/75 to-primary/65 px-3 text-sm font-semibold !text-white shadow-theme-xs shadow-primary/20 backdrop-blur-md transition-all duration-300 before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:h-1/2 before:rounded-t-xl before:bg-gradient-to-b before:from-white/30 before:to-transparent dark:border-white/20 dark:before:from-white/20",
+ tenders:
+ "pagination-tenders-active relative flex h-9 min-w-9 items-center justify-center rounded-xl border border-white/35 !bg-gradient-to-br !from-primary/85 !via-primary/75 !to-primary/65 px-3 text-sm font-semibold !text-white shadow-theme-xs shadow-primary/20 backdrop-blur-md transition-all duration-300 before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:h-1/2 before:rounded-t-xl before:bg-gradient-to-b before:from-white/30 before:to-transparent hover:!border-white/35 hover:!text-white dark:!from-primary/85 dark:!via-primary/75 dark:!to-primary/65 dark:border-white/20 dark:before:from-white/20",
+};
+
+const activeLinkClassNames: Record = {
+ default: undefined,
+ tenders: "!text-white",
+};
+
/** Full-cell hit target: styles live on , but react-paginate puts clicks on the inner . */
const pageLinkClassName =
"absolute inset-0 z-[1] flex cursor-pointer items-center justify-center select-none";
@@ -20,9 +36,10 @@ const Pagination: FC = ({
pageRangeDisplayed = 5,
marginPagesDisplayed = 3,
pageLabelBuilder,
+ variant = "default",
}) => {
return (
-
+
+
);
};
diff --git a/src/css/style.css b/src/css/style.css
index f5105c0..30645ad 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -545,6 +545,24 @@ span.flatpickr-weekday,
@apply !visible;
}
+/* Tenders table — active page (react-paginate merges page + active classes on ) */
+nav[data-pagination-variant="tenders"] li.pagination-tenders-active {
+ background-color: #5750f1 !important;
+ background-image: linear-gradient(
+ to bottom right,
+ rgb(87 80 241 / 0.85),
+ rgb(87 80 241 / 0.75),
+ rgb(87 80 241 / 0.65)
+ ) !important;
+ border-color: rgb(255 255 255 / 0.35) !important;
+ box-shadow: 0 1px 2px 0 rgb(87 80 241 / 0.25) !important;
+ color: #fff !important;
+}
+
+nav[data-pagination-variant="tenders"] li.pagination-tenders-active a {
+ color: #fff !important;
+}
+
.data-table-two .datatable-bottom {
@apply flex flex-col gap-4 px-8 py-7 after:hidden sm:flex-row sm:items-center sm:justify-between sm:gap-0 sm:space-x-4;
}
diff --git a/src/hooks/useHybridPagination.ts b/src/hooks/useHybridPagination.ts
index 4beac92..749abc2 100644
--- a/src/hooks/useHybridPagination.ts
+++ b/src/hooks/useHybridPagination.ts
@@ -19,6 +19,10 @@ type UseHybridPaginationOptions = {
error?: unknown;
};
+/** Each push records the absolute page we were on when this cursor was added,
+ * so a later pop can fall back to that page via offset if needed. */
+type CursorStackEntry = { cursor: string; fromPage: number };
+
export const useHybridPagination = ({
meta,
params,
@@ -26,9 +30,23 @@ export const useHybridPagination = ({
isError,
error,
}: UseHybridPaginationOptions) => {
- const [cursorStack, setCursorStack] = useState([]);
+ const [cursorStack, setCursorStack] = useState([]);
+ const [lastValidPages, setLastValidPages] = useState(1);
+ const [lastValidLimit, setLastValidLimit] = useState(10);
- const limit = meta?.limit ?? Number(params.limit) ?? 10;
+ const limit = meta?.limit ?? Number(params.limit) ?? lastValidLimit;
+
+ useEffect(() => {
+ if (meta?.limit != null) {
+ setLastValidLimit(meta.limit);
+ }
+ }, [meta?.limit]);
+
+ useEffect(() => {
+ if (meta?.pages != null) {
+ setLastValidPages(meta.pages);
+ }
+ }, [meta?.pages]);
const clearCursorStack = useCallback(() => {
setCursorStack([]);
@@ -49,19 +67,40 @@ export const useHybridPagination = ({
}, [isError, error, resetToFirstPage]);
const hasNext = Boolean(meta?.has_more && meta?.next_cursor);
- const hasPrevious = cursorStack.length > 0 || (meta?.page ?? 1) > 1;
+ /** 0-based page index; do not trust meta.page alone while cursor pages are active. */
const currentPageIndex = useMemo(() => {
+ const pageSize = limit > 0 ? limit : 1;
+
+ if (cursorStack.length > 0) {
+ const last = cursorStack[cursorStack.length - 1];
+ return Math.max(0, last.fromPage - 1 + cursorStack.length);
+ }
+
+ const paramsCursor =
+ typeof params.cursor === "string" ? params.cursor.trim() : "";
+ const paramsOffset = Number(params.offset);
+ if (!paramsCursor && Number.isFinite(paramsOffset) && paramsOffset >= 0) {
+ return Math.floor(paramsOffset / pageSize);
+ }
+
+ const metaOffset = Number(meta?.offset);
+ if (Number.isFinite(metaOffset) && metaOffset >= 0) {
+ return Math.floor(metaOffset / pageSize);
+ }
+
if (meta?.page != null) return meta.page - 1;
- return cursorStack.length;
- }, [cursorStack.length, meta?.page]);
+ return 0;
+ }, [cursorStack, params.cursor, params.offset, meta?.offset, meta?.page, limit]);
+
+ const hasPrevious = cursorStack.length > 0 || currentPageIndex > 0;
const pagination = useMemo(
() => ({
currentPage: currentPageIndex,
- totalPages: meta?.pages ?? 1,
+ totalPages: meta?.pages ?? lastValidPages,
}),
- [currentPageIndex, meta?.pages],
+ [currentPageIndex, meta?.pages, lastValidPages],
);
const goToOffsetPage = useCallback(
@@ -80,29 +119,39 @@ export const useHybridPagination = ({
[clearCursorStack, limit, setParams],
);
- const goToNextCursorPage = useCallback(() => {
- const nextCursor = meta?.next_cursor;
- if (!meta?.has_more || !nextCursor) return;
+ const goToNextCursorPage = useCallback(
+ (fromPage1Based: number) => {
+ const nextCursor = meta?.next_cursor;
+ if (!meta?.has_more || !nextCursor) return;
- setCursorStack((stack) => [...stack, nextCursor]);
- setParams((current) => {
- const next: Record = { ...current, cursor: nextCursor, limit };
- delete next.offset;
- return next;
- });
- }, [limit, meta?.has_more, meta?.next_cursor, setParams]);
+ setCursorStack((stack) => [
+ ...stack,
+ { cursor: nextCursor, fromPage: fromPage1Based },
+ ]);
+ setParams((current) => {
+ const next: Record = { ...current, cursor: nextCursor, limit };
+ delete next.offset;
+ return next;
+ });
+ },
+ [limit, meta?.has_more, meta?.next_cursor, setParams],
+ );
const goToPreviousPage = useCallback(() => {
if (cursorStack.length > 0) {
+ const poppedEntry = cursorStack[cursorStack.length - 1];
const nextStack = cursorStack.slice(0, -1);
- const previousCursor = nextStack[nextStack.length - 1];
+ const previousEntry = nextStack[nextStack.length - 1];
setCursorStack(nextStack);
setParams((current) => {
const next: Record = { ...current, limit };
- if (previousCursor) {
- next.cursor = previousCursor;
+ if (previousEntry) {
+ next.cursor = previousEntry.cursor;
delete next.offset;
+ } else if (poppedEntry.fromPage > 1) {
+ next.offset = (poppedEntry.fromPage - 1) * limit;
+ delete next.cursor;
} else {
next.offset = 0;
delete next.cursor;
@@ -112,14 +161,20 @@ export const useHybridPagination = ({
return;
}
- const page = meta?.page ?? 1;
- if (page <= 1) {
+ if (currentPageIndex <= 0) {
resetToFirstPage();
return;
}
- goToOffsetPage(page - 2);
- }, [cursorStack, goToOffsetPage, limit, meta?.page, resetToFirstPage, setParams]);
+ goToOffsetPage(currentPageIndex - 1);
+ }, [
+ cursorStack,
+ currentPageIndex,
+ goToOffsetPage,
+ limit,
+ resetToFirstPage,
+ setParams,
+ ]);
const handlePaginationChange = useCallback(
(e: { selected: number }) => {
@@ -127,24 +182,40 @@ export const useHybridPagination = ({
const delta = targetIndex - currentPageIndex;
if (delta === 1 && hasNext) {
- goToNextCursorPage();
+ goToNextCursorPage(currentPageIndex + 1);
return;
}
- if (delta === -1 && hasPrevious) {
- goToPreviousPage();
- return;
+ if (delta === -1) {
+ const cursorReturnIndex =
+ cursorStack.length > 0
+ ? cursorStack[cursorStack.length - 1].fromPage - 1
+ : null;
+
+ if (
+ cursorReturnIndex != null &&
+ targetIndex === cursorReturnIndex &&
+ cursorStack.length > 0
+ ) {
+ goToPreviousPage();
+ return;
+ }
+
+ if (targetIndex >= 0) {
+ goToOffsetPage(targetIndex);
+ return;
+ }
}
goToOffsetPage(targetIndex);
},
[
currentPageIndex,
+ cursorStack,
goToNextCursorPage,
goToOffsetPage,
goToPreviousPage,
hasNext,
- hasPrevious,
],
);