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, ], );