feat(pagination): implement hybrid pagination across multiple components

- Introduced a new `useHybridPagination` hook to manage pagination logic, enhancing data fetching and navigation.
- Updated various components including `useAdminsPresenter`, `useCompanyListPresenter`, `useCustomerListPresenter`, and others to utilize the new pagination hook, improving code consistency and reducing duplication.
- Refactored pagination handling in each component to streamline state management and improve user experience during data navigation.
- Enhanced search functionality to reset pagination state when new search parameters are applied, ensuring accurate data display.
This commit is contained in:
AmirReza Jamali
2026-05-17 17:01:28 +03:30
parent a6bb481ab1
commit f4a425a6e5
12 changed files with 292 additions and 211 deletions
@@ -8,6 +8,7 @@ import {
useGetAdminListQuery, useGetAdminListQuery,
} from "@/hooks/queries"; } from "@/hooks/queries";
import { IUser, TChangeAdminStatusCredentials } from "@/lib/api"; import { IUser, TChangeAdminStatusCredentials } from "@/lib/api";
import { useHybridPagination } from "@/hooks/useHybridPagination";
import { deleteEmptyKeys, getPaginatedRowNumber } from "@/utils/shared"; import { deleteEmptyKeys, getPaginatedRowNumber } from "@/utils/shared";
import gsap from "gsap"; import gsap from "gsap";
import { useIsMutating } from "@tanstack/react-query"; import { useIsMutating } from "@tanstack/react-query";
@@ -70,25 +71,26 @@ export const useAdminsPresenter = () => {
filterFormReset(newParams); filterFormReset(newParams);
}, [searchParams, filterFormReset]); }, [searchParams, filterFormReset]);
const { data, isPending } = useGetAdminListQuery({ const { data, isPending, isError, error } = useGetAdminListQuery({
...apiDefaultParams, ...apiDefaultParams,
...params, ...params,
}); });
const { pagination, handlePaginationChange, buildFirstPageParams } =
useHybridPagination({
meta: data?.meta,
params: { ...apiDefaultParams, ...params },
setParams,
isError,
error,
});
const shouldShowPagination = const shouldShowPagination =
!isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10; !isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
const usersList = data?.data?.users ?? []; const usersList = data?.data?.users ?? [];
const metadata = data?.meta; const metadata = data?.meta;
const pagination = useMemo(
() => ({
currentPage: data?.meta?.page ? data.meta.page - 1 : 0,
totalPages: data?.meta?.pages ?? 1,
}),
[data?.meta?.page, data?.meta?.pages],
);
const getRowNumber = useCallback( const getRowNumber = useCallback(
(index: number) => (index: number) =>
getPaginatedRowNumber({ getPaginatedRowNumber({
@@ -99,18 +101,6 @@ export const useAdminsPresenter = () => {
[data?.meta?.limit, data?.meta?.page], [data?.meta?.limit, data?.meta?.page],
); );
const handlePaginationChange = useCallback(
(e: { selected: number }) => {
const limit = data?.meta?.limit ?? 10;
setParams((currentParams) => ({
...currentParams,
offset: e.selected * limit,
limit,
}));
},
[data?.meta?.limit, setParams],
);
const navigateToEditAdmin = useCallback( const navigateToEditAdmin = useCallback(
(userId: string) => { (userId: string) => {
router.push(`/admins/edit/${userId}`); router.push(`/admins/edit/${userId}`);
@@ -193,7 +183,7 @@ export const useAdminsPresenter = () => {
}); });
const search = (formData: any) => { const search = (formData: any) => {
const newParams = { ...params, ...formData, offset: 0 }; const newParams = buildFirstPageParams({ ...params, ...formData });
const cleanedParams = deleteEmptyKeys(newParams); const cleanedParams = deleteEmptyKeys(newParams);
const queryString = new URLSearchParams(cleanedParams).toString(); const queryString = new URLSearchParams(cleanedParams).toString();
router.push(`${pathName}?${queryString}`); router.push(`${pathName}?${queryString}`);
@@ -5,6 +5,7 @@ import {
useDeleteCompanyCategoryQuery, useDeleteCompanyCategoryQuery,
useToggleCompanyCategoryPublished, useToggleCompanyCategoryPublished,
} from "@/hooks/queries"; } from "@/hooks/queries";
import { useHybridPagination } from "@/hooks/useHybridPagination";
import { useTableSectionRowAnimations } from "@/hooks/useTableSectionRowAnimations"; import { useTableSectionRowAnimations } from "@/hooks/useTableSectionRowAnimations";
import { TCompanyCategory } from "@/lib/api"; import { TCompanyCategory } from "@/lib/api";
import { deleteEmptyKeys } from "@/utils/shared"; import { deleteEmptyKeys } from "@/utils/shared";
@@ -49,7 +50,16 @@ const useCompanyCategoriesPresenter = () => {
filterFormReset(newParams); filterFormReset(newParams);
}, [searchParams, filterFormReset]); }, [searchParams, filterFormReset]);
const { data, isLoading, isError } = useCompanyCategoriesQuery(params); const { data, isLoading, isError, error } = useCompanyCategoriesQuery(params);
const { pagination, handlePaginationChange, buildFirstPageParams } =
useHybridPagination({
meta: data?.meta,
params,
setParams,
isError,
error,
});
useTableSectionRowAnimations(isLoading, data, { useTableSectionRowAnimations(isLoading, data, {
tableSectionRef, tableSectionRef,
@@ -63,26 +73,6 @@ const useCompanyCategoriesPresenter = () => {
!isLoading && !isLoading &&
(data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10; (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
const pagination = useMemo(
() => ({
currentPage: data?.meta?.page ? data.meta.page - 1 : 0,
totalPages: data?.meta?.pages ?? 1,
}),
[data?.meta?.page, data?.meta?.pages],
);
const handlePaginationChange = useCallback(
(e: { selected: number }) => {
const limit = data?.meta?.limit ?? 10;
setParams((currentParams) => ({
...currentParams,
offset: e.selected * limit,
limit,
}));
},
[data?.meta?.limit],
);
const [optimisticCategories, addOptimisticPublished] = useOptimistic< const [optimisticCategories, addOptimisticPublished] = useOptimistic<
TCompanyCategory[], TCompanyCategory[],
TogglePublishedOptimistic TogglePublishedOptimistic
@@ -96,7 +86,7 @@ const useCompanyCategoriesPresenter = () => {
useToggleCompanyCategoryPublished(); useToggleCompanyCategoryPublished();
const search = (formData: any) => { const search = (formData: any) => {
const newParams = { ...params, ...formData, offset: 0 }; const newParams = buildFirstPageParams({ ...params, ...formData });
const cleanedParams = deleteEmptyKeys(newParams); const cleanedParams = deleteEmptyKeys(newParams);
const queryString = new URLSearchParams(cleanedParams).toString(); const queryString = new URLSearchParams(cleanedParams).toString();
router.push(`${pathName}?${queryString}`); router.push(`${pathName}?${queryString}`);
@@ -5,6 +5,7 @@ import {
useCompaniesQuery, useCompaniesQuery,
useDeleteCompanyQuery, useDeleteCompanyQuery,
} from "@/hooks/queries"; } from "@/hooks/queries";
import { useHybridPagination } from "@/hooks/useHybridPagination";
import { useTableSectionRowAnimations } from "@/hooks/useTableSectionRowAnimations"; import { useTableSectionRowAnimations } from "@/hooks/useTableSectionRowAnimations";
import { TCompany } from "@/lib/api"; import { TCompany } from "@/lib/api";
import { deleteEmptyKeys } from "@/utils/shared"; import { deleteEmptyKeys } from "@/utils/shared";
@@ -65,6 +66,15 @@ export const useCompanyListPresenter = () => {
const { data, error, isPending, isError } = useCompaniesQuery(params); const { data, error, isPending, isError } = useCompaniesQuery(params);
const { pagination, handlePaginationChange, buildFirstPageParams } =
useHybridPagination({
meta: data?.meta,
params,
setParams,
isError,
error,
});
useTableSectionRowAnimations(isPending, data, { useTableSectionRowAnimations(isPending, data, {
tableSectionRef, tableSectionRef,
skeletonTbodyRef, skeletonTbodyRef,
@@ -79,7 +89,7 @@ export const useCompanyListPresenter = () => {
); );
const search = (formData: any) => { const search = (formData: any) => {
const newParams = { ...params, ...formData, offset: 0 }; const newParams = buildFirstPageParams({ ...params, ...formData });
const cleanedParams = deleteEmptyKeys(newParams); const cleanedParams = deleteEmptyKeys(newParams);
const queryString = new URLSearchParams(cleanedParams).toString(); const queryString = new URLSearchParams(cleanedParams).toString();
router.push(`${pathName}?${queryString}`); router.push(`${pathName}?${queryString}`);
@@ -89,26 +99,6 @@ export const useCompanyListPresenter = () => {
router.push(pathName); router.push(pathName);
}, [pathName, router]); }, [pathName, router]);
const pagination = useMemo(
() => ({
currentPage: data?.meta?.page ? data.meta.page - 1 : 0,
totalPages: data?.meta?.pages ?? 1,
}),
[data?.meta?.page, data?.meta?.pages],
);
const handlePaginationChange = useCallback(
(e: { selected: number }) => {
const limit = data?.meta?.limit ?? 10;
setParams((currentParams) => ({
...currentParams,
offset: e.selected * limit,
limit,
}));
},
[data?.meta?.limit],
);
const onConfirmDelete = () => { const onConfirmDelete = () => {
if (currentCompany) { if (currentCompany) {
deleteCompany(currentCompany.id); deleteCompany(currentCompany.id);
@@ -4,6 +4,7 @@ import {
useDeleteContactUs, useDeleteContactUs,
useReadAllContactUs, useReadAllContactUs,
} from "@/hooks/queries/useContactUsQuery"; } from "@/hooks/queries/useContactUsQuery";
import { useHybridPagination } from "@/hooks/useHybridPagination";
import { useTableSectionRowAnimations } from "@/hooks/useTableSectionRowAnimations"; import { useTableSectionRowAnimations } from "@/hooks/useTableSectionRowAnimations";
import { TContact } from "@/lib/api/types/TContacts"; import { TContact } from "@/lib/api/types/TContacts";
import { deleteEmptyKeys } from "@/utils/shared"; import { deleteEmptyKeys } from "@/utils/shared";
@@ -55,7 +56,16 @@ export const useContactUsListPresenter = () => {
filterFormReset(newParams as any); filterFormReset(newParams as any);
}, [searchParams, filterFormReset]); }, [searchParams, filterFormReset]);
const { data, isPending } = useReadAllContactUs(params); const { data, isPending, isError, error } = useReadAllContactUs(params);
const { pagination, handlePaginationChange, buildFirstPageParams } =
useHybridPagination({
meta: data?.meta,
params,
setParams,
isError,
error,
});
useTableSectionRowAnimations(isPending, data, { useTableSectionRowAnimations(isPending, data, {
tableSectionRef, tableSectionRef,
@@ -70,28 +80,8 @@ export const useContactUsListPresenter = () => {
const shouldShowPagination = const shouldShowPagination =
!isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10; !isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
const pagination = useMemo(
() => ({
currentPage: data?.meta?.page ? data.meta.page - 1 : 0,
totalPages: data?.meta?.pages ?? 1,
}),
[data?.meta?.page, data?.meta?.pages],
);
const handlePaginationChange = useCallback(
(e: { selected: number }) => {
const limit = data?.meta?.limit ?? 10;
setParams((currentParams) => ({
...currentParams,
offset: e.selected * limit,
limit,
}));
},
[data?.meta?.limit],
);
const search = (formData: any) => { const search = (formData: any) => {
const newParams = { ...params, ...formData, offset: 0 }; const newParams = buildFirstPageParams({ ...params, ...formData });
const cleanedParams = deleteEmptyKeys(newParams); const cleanedParams = deleteEmptyKeys(newParams);
const queryString = new URLSearchParams( const queryString = new URLSearchParams(
cleanedParams as Record<string, string>, cleanedParams as Record<string, string>,
@@ -6,6 +6,7 @@ import {
useCustomersQuery, useCustomersQuery,
useDeleteCustomerQuery, useDeleteCustomerQuery,
} from "@/hooks/queries"; } from "@/hooks/queries";
import { useHybridPagination } from "@/hooks/useHybridPagination";
import { useTableSectionRowAnimations } from "@/hooks/useTableSectionRowAnimations"; import { useTableSectionRowAnimations } from "@/hooks/useTableSectionRowAnimations";
import { TAssignCustomerToCompanyCredentials, TCustomer } from "@/lib/api"; import { TAssignCustomerToCompanyCredentials, TCustomer } from "@/lib/api";
import { apiDefaultParams } from "@/constants/Api_Params"; import { apiDefaultParams } from "@/constants/Api_Params";
@@ -59,7 +60,16 @@ const useCustomerListPresenter = () => {
filterFormReset(newParams); filterFormReset(newParams);
}, [searchParams, filterFormReset]); }, [searchParams, filterFormReset]);
const { data, isPending } = useCustomersQuery(params); const { data, isPending, isError, error } = useCustomersQuery(params);
const { pagination, handlePaginationChange, buildFirstPageParams } =
useHybridPagination({
meta: data?.meta,
params,
setParams,
isError,
error,
});
useTableSectionRowAnimations(isPending, data, { useTableSectionRowAnimations(isPending, data, {
tableSectionRef, tableSectionRef,
@@ -81,7 +91,7 @@ const useCustomerListPresenter = () => {
); );
const search = (formData: any) => { const search = (formData: any) => {
const newParams = { ...params, ...formData, offset: 0 }; const newParams = buildFirstPageParams({ ...params, ...formData });
const cleanedParams = deleteEmptyKeys(newParams); const cleanedParams = deleteEmptyKeys(newParams);
const queryString = new URLSearchParams(cleanedParams).toString(); const queryString = new URLSearchParams(cleanedParams).toString();
router.push(`${pathName}?${queryString}`); router.push(`${pathName}?${queryString}`);
@@ -112,26 +122,6 @@ const useCustomerListPresenter = () => {
const shouldShowPagination = const shouldShowPagination =
!isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10; !isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
const pagination = useMemo(
() => ({
currentPage: data?.meta?.page ? data.meta.page - 1 : 0,
totalPages: data?.meta?.pages ?? 1,
}),
[data?.meta?.page, data?.meta?.pages],
);
const handlePaginationChange = useCallback(
(e: { selected: number }) => {
const limit = data?.meta?.limit ?? 10;
setParams((currentParams) => ({
...currentParams,
offset: e.selected * limit,
limit,
}));
},
[data?.meta?.limit],
);
return { return {
allCustomers: customers, allCustomers: customers,
metadata: data?.meta, metadata: data?.meta,
@@ -1,6 +1,7 @@
"use client"; "use client";
import { apiDefaultParams } from "@/constants/Api_Params"; import { apiDefaultParams } from "@/constants/Api_Params";
import { useGetFeedback } from "@/hooks/queries"; import { useGetFeedback } from "@/hooks/queries";
import { useHybridPagination } from "@/hooks/useHybridPagination";
import { useTableSectionRowAnimations } from "@/hooks/useTableSectionRowAnimations"; import { useTableSectionRowAnimations } from "@/hooks/useTableSectionRowAnimations";
import { deleteEmptyKeys } from "@/utils/shared"; import { deleteEmptyKeys } from "@/utils/shared";
import { useIsMutating } from "@tanstack/react-query"; import { useIsMutating } from "@tanstack/react-query";
@@ -50,7 +51,16 @@ const useFeedbackTablePresenter = ({ id, paramKey }: IProps) => {
[params, paramKey, id], [params, paramKey, id],
); );
const { data, isLoading } = useGetFeedback(queryParams); const { data, isLoading, isError, error } = useGetFeedback(queryParams);
const { pagination, handlePaginationChange, buildFirstPageParams } =
useHybridPagination({
meta: data?.meta,
params: queryParams,
setParams,
isError,
error,
});
useTableSectionRowAnimations(isLoading, data, { useTableSectionRowAnimations(isLoading, data, {
tableSectionRef, tableSectionRef,
@@ -70,28 +80,8 @@ const useFeedbackTablePresenter = ({ id, paramKey }: IProps) => {
!isLoading && !isLoading &&
(data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10; (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
const pagination = useMemo(
() => ({
currentPage: data?.meta?.page ? data.meta.page - 1 : 0,
totalPages: data?.meta?.pages ?? 1,
}),
[data?.meta?.page, data?.meta?.pages],
);
const handlePaginationChange = useCallback(
(e: { selected: number }) => {
const limit = data?.meta?.limit ?? 10;
setParams((currentParams) => ({
...currentParams,
offset: e.selected * limit,
limit,
}));
},
[data?.meta?.limit],
);
const search = (formData: any) => { const search = (formData: any) => {
const next = { ...params, ...formData, offset: 0 }; const next = buildFirstPageParams({ ...params, ...formData });
const cleaned = deleteEmptyKeys(next); const cleaned = deleteEmptyKeys(next);
const queryString = new URLSearchParams( const queryString = new URLSearchParams(
cleaned as Record<string, string>, cleaned as Record<string, string>,
@@ -4,6 +4,7 @@ import {
useDeleteInquiry, useDeleteInquiry,
useGetInquiries, useGetInquiries,
} from "@/hooks/queries/useInquiryQueries"; } from "@/hooks/queries/useInquiryQueries";
import { useHybridPagination } from "@/hooks/useHybridPagination";
import { useTableSectionRowAnimations } from "@/hooks/useTableSectionRowAnimations"; import { useTableSectionRowAnimations } from "@/hooks/useTableSectionRowAnimations";
import { TInquiries } from "@/lib/api"; import { TInquiries } from "@/lib/api";
import { deleteEmptyKeys } from "@/utils/shared"; import { deleteEmptyKeys } from "@/utils/shared";
@@ -55,7 +56,16 @@ const useInquiriesListPresenter = () => {
filterFormReset(newParams); filterFormReset(newParams);
}, [searchParams, filterFormReset]); }, [searchParams, filterFormReset]);
const { data, isPending } = useGetInquiries(params); const { data, isPending, isError, error } = useGetInquiries(params);
const { pagination, handlePaginationChange, buildFirstPageParams } =
useHybridPagination({
meta: data?.meta,
params,
setParams,
isError,
error,
});
useTableSectionRowAnimations(isPending, data, { useTableSectionRowAnimations(isPending, data, {
tableSectionRef, tableSectionRef,
@@ -66,32 +76,12 @@ const useInquiriesListPresenter = () => {
const shouldShowPagination = const shouldShowPagination =
!isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10; !isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
const pagination = useMemo(
() => ({
currentPage: data?.meta?.page ? data.meta.page - 1 : 0,
totalPages: data?.meta?.pages ?? 1,
}),
[data?.meta?.page, data?.meta?.pages],
);
const handlePaginationChange = useCallback(
(e: { selected: number }) => {
const limit = data?.meta?.limit ?? 10;
setParams((currentParams) => ({
...currentParams,
offset: e.selected * limit,
limit,
}));
},
[data?.meta?.limit],
);
const { mutate } = useDeleteInquiry(() => setIsDeleteModalOpen(false)); const { mutate } = useDeleteInquiry(() => setIsDeleteModalOpen(false));
const { mutate: changeStatus } = const { mutate: changeStatus } =
useChangeInquiryStatus(() => setIsChangeStatusModalOpen(false)); useChangeInquiryStatus(() => setIsChangeStatusModalOpen(false));
const search = (formData: any) => { const search = (formData: any) => {
const newParams = { ...params, ...formData, offset: 0 }; const newParams = buildFirstPageParams({ ...params, ...formData });
const cleanedParams = deleteEmptyKeys(newParams); const cleanedParams = deleteEmptyKeys(newParams);
const queryString = new URLSearchParams(cleanedParams).toString(); const queryString = new URLSearchParams(cleanedParams).toString();
router.push(`${pathname}?${queryString}`); router.push(`${pathname}?${queryString}`);
@@ -1,6 +1,7 @@
"use client"; "use client";
import { apiDefaultParams } from "@/constants/Api_Params"; import { apiDefaultParams } from "@/constants/Api_Params";
import { useGetNotificationHistoryQuery } from "@/hooks/queries/useNotificationQueries"; import { useGetNotificationHistoryQuery } from "@/hooks/queries/useNotificationQueries";
import { useHybridPagination } from "@/hooks/useHybridPagination";
import { useTableSectionRowAnimations } from "@/hooks/useTableSectionRowAnimations"; import { useTableSectionRowAnimations } from "@/hooks/useTableSectionRowAnimations";
import { deleteEmptyKeys } from "@/utils/shared"; import { deleteEmptyKeys } from "@/utils/shared";
import { useIsMutating } from "@tanstack/react-query"; import { useIsMutating } from "@tanstack/react-query";
@@ -24,7 +25,17 @@ const useNotificationHistoryTablePresenter = () => {
const skeletonTbodyRef = useRef<HTMLTableSectionElement>(null); const skeletonTbodyRef = useRef<HTMLTableSectionElement>(null);
const dataTbodyRef = useRef<HTMLTableSectionElement>(null); const dataTbodyRef = useRef<HTMLTableSectionElement>(null);
const { data, isLoading } = useGetNotificationHistoryQuery(params); const { data, isLoading, isError, error } =
useGetNotificationHistoryQuery(params);
const { pagination, handlePaginationChange, buildFirstPageParams } =
useHybridPagination({
meta: data?.meta,
params,
setParams,
isError,
error,
});
useTableSectionRowAnimations(isLoading, data, { useTableSectionRowAnimations(isLoading, data, {
tableSectionRef, tableSectionRef,
@@ -64,28 +75,8 @@ const useNotificationHistoryTablePresenter = () => {
filterFormReset(newParams); filterFormReset(newParams);
}, [searchParams, filterFormReset]); }, [searchParams, filterFormReset]);
const pagination = useMemo(
() => ({
currentPage: data?.meta?.page ? data.meta.page - 1 : 0,
totalPages: data?.meta?.pages ?? 1,
}),
[data?.meta?.page, data?.meta?.pages],
);
const handlePaginationChange = useCallback(
(e: { selected: number }) => {
const limit = data?.meta?.limit ?? 10;
setParams((currentParams: Record<string, any>) => ({
...currentParams,
offset: e.selected * limit,
limit,
}));
},
[data?.meta?.limit],
);
const search = (formData: any) => { const search = (formData: any) => {
const newParams = { ...params, ...formData, offset: 0 }; const newParams = buildFirstPageParams({ ...params, ...formData });
const cleanedParams = deleteEmptyKeys(newParams); const cleanedParams = deleteEmptyKeys(newParams);
const queryString = new URLSearchParams(cleanedParams).toString(); const queryString = new URLSearchParams(cleanedParams).toString();
router.push(`${pathName}?${queryString}`); router.push(`${pathName}?${queryString}`);
@@ -2,6 +2,7 @@
import { ConfirmationModalProps } from "@/components/ui/ConfirmationModal"; import { ConfirmationModalProps } from "@/components/ui/ConfirmationModal";
import { apiDefaultParams } from "@/constants/Api_Params"; import { apiDefaultParams } from "@/constants/Api_Params";
import { useTendersQuery } from "@/hooks/queries"; import { useTendersQuery } from "@/hooks/queries";
import { useHybridPagination } from "@/hooks/useHybridPagination";
import { TCustomer } from "@/lib/api"; import { TCustomer } from "@/lib/api";
import { deleteEmptyKeys, getPaginatedRowNumber, unixToDate } from "@/utils/shared"; import { deleteEmptyKeys, getPaginatedRowNumber, unixToDate } from "@/utils/shared";
import gsap from "gsap"; import gsap from "gsap";
@@ -284,7 +285,17 @@ const useTenderListPresenter = () => {
filterFormReset(mergeTenderFilterFormState(newParams)); filterFormReset(mergeTenderFilterFormState(newParams));
}, [searchParams, filterFormReset]); }, [searchParams, filterFormReset]);
const { data, error, isPending } = useTendersQuery(params); const { data, error, isPending, isError } = useTendersQuery(params);
const { pagination, handlePaginationChange, buildFirstPageParams } =
useHybridPagination({
meta: data?.meta,
params,
setParams,
isError,
error,
});
const shouldShowPagination = const shouldShowPagination =
!isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10; !isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
@@ -311,18 +322,6 @@ const useTenderListPresenter = () => {
[pathName, router, langQuerySuffix], [pathName, router, langQuerySuffix],
); );
const handlePaginationChange = useCallback(
(e: { selected: number }) => {
const limit = data?.meta?.limit ?? 10;
setParams((currentParams) => ({
...currentParams,
offset: e.selected * limit,
limit,
}));
},
[data?.meta?.limit, setParams],
);
const getRowNumber = useCallback( const getRowNumber = useCallback(
(index: number) => (index: number) =>
getPaginatedRowNumber({ getPaginatedRowNumber({
@@ -337,14 +336,6 @@ const useTenderListPresenter = () => {
return unixToDate({ unix, hasTime: true }); return unixToDate({ unix, hasTime: true });
}, []); }, []);
const pagination = useMemo(
() => ({
currentPage: data?.meta?.page ? data.meta.page - 1 : 0,
totalPages: data?.meta?.pages ?? 1,
}),
[data?.meta?.page, data?.meta?.pages],
);
useLayoutEffect(() => { useLayoutEffect(() => {
const reduced = const reduced =
typeof window !== "undefined" && typeof window !== "undefined" &&
@@ -401,21 +392,20 @@ const useTenderListPresenter = () => {
delete merged[key]; delete merged[key];
} }
} }
const newParams = { ...merged, ...normalizedData, offset: 0 }; const newParams = buildFirstPageParams({ ...merged, ...normalizedData });
const cleanedParams = deleteEmptyKeys(newParams); const cleanedParams = deleteEmptyKeys(newParams);
const queryString = new URLSearchParams(cleanedParams).toString(); const queryString = new URLSearchParams(cleanedParams).toString();
router.push(`${pathName}?${queryString}`); router.push(`${pathName}?${queryString}`);
}; };
const clearAllFilters = useCallback(() => { const clearAllFilters = useCallback(() => {
const cleared: Record<string, any> = { const cleared = buildFirstPageParams({
...apiDefaultParams, ...apiDefaultParams,
offset: 0,
limit: 10, limit: 10,
}; });
filterFormReset(mergeTenderFilterFormState(cleared)); filterFormReset(mergeTenderFilterFormState(cleared));
router.push(pathName); router.push(pathName);
}, [pathName, router, filterFormReset]); }, [buildFirstPageParams, pathName, router, filterFormReset]);
const columns = [ const columns = [
"row", "row",
"title", "title",
+2 -1
View File
@@ -14,8 +14,9 @@ const Pagination: FC<IProps> = ({ totalPages, currentPage, onPageChange }) => {
return ( return (
<Fragment> <Fragment>
<ReactPaginate <ReactPaginate
key={currentPage}
pageCount={totalPages} pageCount={totalPages}
initialPage={currentPage} forcePage={currentPage}
nextLabel="" nextLabel=""
previousLabel="" previousLabel=""
pageRangeDisplayed={5} pageRangeDisplayed={5}
+171
View File
@@ -0,0 +1,171 @@
"use client";
import { IMetaData } from "@/lib/api/types/Factory";
import { AxiosError } from "axios";
import { useCallback, useEffect, useMemo, useState } from "react";
const isInvalidCursorError = (error: unknown) => {
const axiosError = error as AxiosError<{ error?: { message?: string } }>;
if (axiosError.response?.status !== 400) return false;
const message = axiosError.response?.data?.error?.message ?? "";
return /cursor|pagination/i.test(message);
};
type UseHybridPaginationOptions = {
meta?: IMetaData;
params: Record<string, unknown>;
setParams: React.Dispatch<React.SetStateAction<Record<string, any>>>;
isError?: boolean;
error?: unknown;
};
export const useHybridPagination = ({
meta,
params,
setParams,
isError,
error,
}: UseHybridPaginationOptions) => {
const [cursorStack, setCursorStack] = useState<string[]>([]);
const limit = meta?.limit ?? Number(params.limit) ?? 10;
const clearCursorStack = useCallback(() => {
setCursorStack([]);
}, []);
const resetToFirstPage = useCallback(() => {
clearCursorStack();
setParams((current) => {
const next: Record<string, any> = { ...current, offset: 0, limit };
delete next.cursor;
return next;
});
}, [clearCursorStack, limit, setParams]);
useEffect(() => {
if (!isError || !error || !isInvalidCursorError(error)) return;
resetToFirstPage();
}, [isError, error, resetToFirstPage]);
const hasNext = Boolean(meta?.has_more && meta?.next_cursor);
const hasPrevious = cursorStack.length > 0 || (meta?.page ?? 1) > 1;
const currentPageIndex = useMemo(() => {
if (meta?.page != null) return meta.page - 1;
return cursorStack.length;
}, [cursorStack.length, meta?.page]);
const pagination = useMemo(
() => ({
currentPage: currentPageIndex,
totalPages: meta?.pages ?? 1,
}),
[currentPageIndex, meta?.pages],
);
const goToOffsetPage = useCallback(
(pageIndex: number) => {
clearCursorStack();
setParams((current) => {
const next: Record<string, any> = {
...current,
offset: pageIndex * limit,
limit,
};
delete next.cursor;
return next;
});
},
[clearCursorStack, limit, setParams],
);
const goToNextCursorPage = useCallback(() => {
const nextCursor = meta?.next_cursor;
if (!meta?.has_more || !nextCursor) return;
setCursorStack((stack) => [...stack, nextCursor]);
setParams((current) => {
const next: Record<string, any> = { ...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 nextStack = cursorStack.slice(0, -1);
const previousCursor = nextStack[nextStack.length - 1];
setCursorStack(nextStack);
setParams((current) => {
const next: Record<string, any> = { ...current, limit };
if (previousCursor) {
next.cursor = previousCursor;
delete next.offset;
} else {
next.offset = 0;
delete next.cursor;
}
return next;
});
return;
}
const page = meta?.page ?? 1;
if (page <= 1) {
resetToFirstPage();
return;
}
goToOffsetPage(page - 2);
}, [cursorStack, goToOffsetPage, limit, meta?.page, resetToFirstPage, setParams]);
const handlePaginationChange = useCallback(
(e: { selected: number }) => {
const targetIndex = e.selected;
const delta = targetIndex - currentPageIndex;
if (delta === 1 && hasNext) {
goToNextCursorPage();
return;
}
if (delta === -1 && hasPrevious) {
goToPreviousPage();
return;
}
goToOffsetPage(targetIndex);
},
[
currentPageIndex,
goToNextCursorPage,
goToOffsetPage,
goToPreviousPage,
hasNext,
hasPrevious,
],
);
const buildFirstPageParams = useCallback(
(base: Record<string, any>) => {
const next: Record<string, any> = { ...base, offset: 0, limit };
delete next.cursor;
clearCursorStack();
return next;
},
[clearCursorStack, limit],
);
return {
pagination,
handlePaginationChange,
buildFirstPageParams,
resetToFirstPage,
clearCursorStack,
currentPageIndex,
hasNext,
hasPrevious,
};
};
+7 -9
View File
@@ -1,26 +1,24 @@
import { z } from "zod"; import { z } from "zod";
const MetaDataSchema = z.object({ const MetaDataSchema = z.object({
limit: z.number(), limit: z.number(),
offset: z.number(), offset: z.number(),
page: z.number(), page: z.number(),
pages: z.number(), pages: z.number(),
total: z.number(), total: z.number(),
has_more: z.boolean().optional(),
next_cursor: z.string().optional(),
prev_cursor: z.string().optional(),
}); });
export function createApiResponseSchema<T extends z.ZodTypeAny>(dataSchema: T) { export function createApiResponseSchema<T extends z.ZodTypeAny>(dataSchema: T) {
return z.object({ return z.object({
data: dataSchema, data: dataSchema,
meta: z meta: MetaDataSchema.optional(),
.object({
limit: z.number(),
offset: z.number(),
page: z.number(),
pages: z.number(),
total: z.number(),
})
.optional(),
message: z.string(), message: z.string(),
code: z.number().optional(), code: z.number().optional(),
success: z.boolean(), success: z.boolean(),
}); });
} }
export type IMetaData = z.infer<typeof MetaDataSchema>; export type IMetaData = z.infer<typeof MetaDataSchema>;