feat(tenders): enhance pagination functionality and styling
- Added absoluteCurrentPage to the useTenderListPresenter for better pagination state management. - Updated the TendersTable component to utilize absoluteCurrentPage in the pagination key. - Enhanced the Pagination component to support a new "tenders" variant for improved styling. - Introduced custom styles for the active page in the tenders pagination, ensuring a distinct visual representation. - Refactored the useHybridPagination hook to maintain accurate page tracking with cursor navigation.
This commit is contained in:
@@ -46,6 +46,7 @@ const TendersTable = () => {
|
|||||||
getRowNumber,
|
getRowNumber,
|
||||||
formatDateTimeCell,
|
formatDateTimeCell,
|
||||||
pagination,
|
pagination,
|
||||||
|
absoluteCurrentPage,
|
||||||
} = useTenderListPresenter();
|
} = useTenderListPresenter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -155,12 +156,14 @@ const TendersTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
<IsVisible condition={shouldShowPagination}>
|
<IsVisible condition={shouldShowPagination}>
|
||||||
<Pagination
|
<Pagination
|
||||||
|
key={`pagination-tenders-${absoluteCurrentPage}`}
|
||||||
currentPage={pagination.currentPage}
|
currentPage={pagination.currentPage}
|
||||||
totalPages={pagination.totalPages}
|
totalPages={pagination.totalPages}
|
||||||
onPageChange={handlePaginationChange}
|
onPageChange={handlePaginationChange}
|
||||||
pageRangeDisplayed={3}
|
pageRangeDisplayed={3}
|
||||||
marginPagesDisplayed={1}
|
marginPagesDisplayed={1}
|
||||||
pageLabelBuilder={buildWindowedPageLabel}
|
pageLabelBuilder={buildWindowedPageLabel}
|
||||||
|
variant="tenders"
|
||||||
/>
|
/>
|
||||||
</IsVisible>
|
</IsVisible>
|
||||||
</ListWrapper>
|
</ListWrapper>
|
||||||
|
|||||||
@@ -539,6 +539,7 @@ const useTenderListPresenter = () => {
|
|||||||
getRowNumber,
|
getRowNumber,
|
||||||
formatDateTimeCell,
|
formatDateTimeCell,
|
||||||
pagination: windowedPagination,
|
pagination: windowedPagination,
|
||||||
|
absoluteCurrentPage: pagination.currentPage,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { FC, Fragment } from "react";
|
import { FC } from "react";
|
||||||
import ReactPaginate, { ReactPaginateProps } from "react-paginate";
|
import ReactPaginate, { ReactPaginateProps } from "react-paginate";
|
||||||
|
|
||||||
|
type PaginationVariant = "default" | "tenders";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
@@ -7,8 +10,21 @@ interface IProps {
|
|||||||
pageRangeDisplayed?: number;
|
pageRangeDisplayed?: number;
|
||||||
marginPagesDisplayed?: number;
|
marginPagesDisplayed?: number;
|
||||||
pageLabelBuilder?: ReactPaginateProps["pageLabelBuilder"];
|
pageLabelBuilder?: ReactPaginateProps["pageLabelBuilder"];
|
||||||
|
variant?: PaginationVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const activeClassNames: Record<PaginationVariant, string> = {
|
||||||
|
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<PaginationVariant, string | undefined> = {
|
||||||
|
default: undefined,
|
||||||
|
tenders: "!text-white",
|
||||||
|
};
|
||||||
|
|
||||||
/** Full-cell hit target: styles live on <li>, but react-paginate puts clicks on the inner <a>. */
|
/** Full-cell hit target: styles live on <li>, but react-paginate puts clicks on the inner <a>. */
|
||||||
const pageLinkClassName =
|
const pageLinkClassName =
|
||||||
"absolute inset-0 z-[1] flex cursor-pointer items-center justify-center select-none";
|
"absolute inset-0 z-[1] flex cursor-pointer items-center justify-center select-none";
|
||||||
@@ -20,9 +36,10 @@ const Pagination: FC<IProps> = ({
|
|||||||
pageRangeDisplayed = 5,
|
pageRangeDisplayed = 5,
|
||||||
marginPagesDisplayed = 3,
|
marginPagesDisplayed = 3,
|
||||||
pageLabelBuilder,
|
pageLabelBuilder,
|
||||||
|
variant = "default",
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<nav data-pagination-variant={variant} aria-label="Pagination">
|
||||||
<ReactPaginate
|
<ReactPaginate
|
||||||
key={currentPage}
|
key={currentPage}
|
||||||
pageCount={totalPages}
|
pageCount={totalPages}
|
||||||
@@ -32,7 +49,8 @@ const Pagination: FC<IProps> = ({
|
|||||||
pageRangeDisplayed={pageRangeDisplayed}
|
pageRangeDisplayed={pageRangeDisplayed}
|
||||||
marginPagesDisplayed={marginPagesDisplayed}
|
marginPagesDisplayed={marginPagesDisplayed}
|
||||||
pageLabelBuilder={pageLabelBuilder}
|
pageLabelBuilder={pageLabelBuilder}
|
||||||
activeClassName="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"
|
activeClassName={activeClassNames[variant]}
|
||||||
|
activeLinkClassName={activeLinkClassNames[variant]}
|
||||||
onPageChange={(e) => {
|
onPageChange={(e) => {
|
||||||
if (!onPageChange) return;
|
if (!onPageChange) return;
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
@@ -46,7 +64,7 @@ const Pagination: FC<IProps> = ({
|
|||||||
previousLinkClassName={pageLinkClassName}
|
previousLinkClassName={pageLinkClassName}
|
||||||
nextLinkClassName={pageLinkClassName}
|
nextLinkClassName={pageLinkClassName}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</nav>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -545,6 +545,24 @@ span.flatpickr-weekday,
|
|||||||
@apply !visible;
|
@apply !visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tenders table — active page (react-paginate merges page + active classes on <li>) */
|
||||||
|
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 {
|
.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;
|
@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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ type UseHybridPaginationOptions = {
|
|||||||
error?: unknown;
|
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 = ({
|
export const useHybridPagination = ({
|
||||||
meta,
|
meta,
|
||||||
params,
|
params,
|
||||||
@@ -26,9 +30,23 @@ export const useHybridPagination = ({
|
|||||||
isError,
|
isError,
|
||||||
error,
|
error,
|
||||||
}: UseHybridPaginationOptions) => {
|
}: UseHybridPaginationOptions) => {
|
||||||
const [cursorStack, setCursorStack] = useState<string[]>([]);
|
const [cursorStack, setCursorStack] = useState<CursorStackEntry[]>([]);
|
||||||
|
const [lastValidPages, setLastValidPages] = useState<number>(1);
|
||||||
|
const [lastValidLimit, setLastValidLimit] = useState<number>(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(() => {
|
const clearCursorStack = useCallback(() => {
|
||||||
setCursorStack([]);
|
setCursorStack([]);
|
||||||
@@ -49,19 +67,40 @@ export const useHybridPagination = ({
|
|||||||
}, [isError, error, resetToFirstPage]);
|
}, [isError, error, resetToFirstPage]);
|
||||||
|
|
||||||
const hasNext = Boolean(meta?.has_more && meta?.next_cursor);
|
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 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;
|
if (meta?.page != null) return meta.page - 1;
|
||||||
return cursorStack.length;
|
return 0;
|
||||||
}, [cursorStack.length, meta?.page]);
|
}, [cursorStack, params.cursor, params.offset, meta?.offset, meta?.page, limit]);
|
||||||
|
|
||||||
|
const hasPrevious = cursorStack.length > 0 || currentPageIndex > 0;
|
||||||
|
|
||||||
const pagination = useMemo(
|
const pagination = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
currentPage: currentPageIndex,
|
currentPage: currentPageIndex,
|
||||||
totalPages: meta?.pages ?? 1,
|
totalPages: meta?.pages ?? lastValidPages,
|
||||||
}),
|
}),
|
||||||
[currentPageIndex, meta?.pages],
|
[currentPageIndex, meta?.pages, lastValidPages],
|
||||||
);
|
);
|
||||||
|
|
||||||
const goToOffsetPage = useCallback(
|
const goToOffsetPage = useCallback(
|
||||||
@@ -80,29 +119,39 @@ export const useHybridPagination = ({
|
|||||||
[clearCursorStack, limit, setParams],
|
[clearCursorStack, limit, setParams],
|
||||||
);
|
);
|
||||||
|
|
||||||
const goToNextCursorPage = useCallback(() => {
|
const goToNextCursorPage = useCallback(
|
||||||
const nextCursor = meta?.next_cursor;
|
(fromPage1Based: number) => {
|
||||||
if (!meta?.has_more || !nextCursor) return;
|
const nextCursor = meta?.next_cursor;
|
||||||
|
if (!meta?.has_more || !nextCursor) return;
|
||||||
|
|
||||||
setCursorStack((stack) => [...stack, nextCursor]);
|
setCursorStack((stack) => [
|
||||||
setParams((current) => {
|
...stack,
|
||||||
const next: Record<string, any> = { ...current, cursor: nextCursor, limit };
|
{ cursor: nextCursor, fromPage: fromPage1Based },
|
||||||
delete next.offset;
|
]);
|
||||||
return next;
|
setParams((current) => {
|
||||||
});
|
const next: Record<string, any> = { ...current, cursor: nextCursor, limit };
|
||||||
}, [limit, meta?.has_more, meta?.next_cursor, setParams]);
|
delete next.offset;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[limit, meta?.has_more, meta?.next_cursor, setParams],
|
||||||
|
);
|
||||||
|
|
||||||
const goToPreviousPage = useCallback(() => {
|
const goToPreviousPage = useCallback(() => {
|
||||||
if (cursorStack.length > 0) {
|
if (cursorStack.length > 0) {
|
||||||
|
const poppedEntry = cursorStack[cursorStack.length - 1];
|
||||||
const nextStack = cursorStack.slice(0, -1);
|
const nextStack = cursorStack.slice(0, -1);
|
||||||
const previousCursor = nextStack[nextStack.length - 1];
|
const previousEntry = nextStack[nextStack.length - 1];
|
||||||
|
|
||||||
setCursorStack(nextStack);
|
setCursorStack(nextStack);
|
||||||
setParams((current) => {
|
setParams((current) => {
|
||||||
const next: Record<string, any> = { ...current, limit };
|
const next: Record<string, any> = { ...current, limit };
|
||||||
if (previousCursor) {
|
if (previousEntry) {
|
||||||
next.cursor = previousCursor;
|
next.cursor = previousEntry.cursor;
|
||||||
delete next.offset;
|
delete next.offset;
|
||||||
|
} else if (poppedEntry.fromPage > 1) {
|
||||||
|
next.offset = (poppedEntry.fromPage - 1) * limit;
|
||||||
|
delete next.cursor;
|
||||||
} else {
|
} else {
|
||||||
next.offset = 0;
|
next.offset = 0;
|
||||||
delete next.cursor;
|
delete next.cursor;
|
||||||
@@ -112,14 +161,20 @@ export const useHybridPagination = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = meta?.page ?? 1;
|
if (currentPageIndex <= 0) {
|
||||||
if (page <= 1) {
|
|
||||||
resetToFirstPage();
|
resetToFirstPage();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
goToOffsetPage(page - 2);
|
goToOffsetPage(currentPageIndex - 1);
|
||||||
}, [cursorStack, goToOffsetPage, limit, meta?.page, resetToFirstPage, setParams]);
|
}, [
|
||||||
|
cursorStack,
|
||||||
|
currentPageIndex,
|
||||||
|
goToOffsetPage,
|
||||||
|
limit,
|
||||||
|
resetToFirstPage,
|
||||||
|
setParams,
|
||||||
|
]);
|
||||||
|
|
||||||
const handlePaginationChange = useCallback(
|
const handlePaginationChange = useCallback(
|
||||||
(e: { selected: number }) => {
|
(e: { selected: number }) => {
|
||||||
@@ -127,24 +182,40 @@ export const useHybridPagination = ({
|
|||||||
const delta = targetIndex - currentPageIndex;
|
const delta = targetIndex - currentPageIndex;
|
||||||
|
|
||||||
if (delta === 1 && hasNext) {
|
if (delta === 1 && hasNext) {
|
||||||
goToNextCursorPage();
|
goToNextCursorPage(currentPageIndex + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (delta === -1 && hasPrevious) {
|
if (delta === -1) {
|
||||||
goToPreviousPage();
|
const cursorReturnIndex =
|
||||||
return;
|
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);
|
goToOffsetPage(targetIndex);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
currentPageIndex,
|
currentPageIndex,
|
||||||
|
cursorStack,
|
||||||
goToNextCursorPage,
|
goToNextCursorPage,
|
||||||
goToOffsetPage,
|
goToOffsetPage,
|
||||||
goToPreviousPage,
|
goToPreviousPage,
|
||||||
hasNext,
|
hasNext,
|
||||||
hasPrevious,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user