feat(Tables): implement pagination and loading state handling across multiple tables

- Added `getPaginatedRowNumber` utility function to calculate the correct row number based on pagination.
- Integrated `isLoading` prop in `EmptyListWrapper` to conditionally render loading state.
- Updated various table components (Admins, CMS, Companies, Inquiries, etc.) to utilize the new pagination logic and loading state handling.
- Wrapped pagination components in `IsVisible` to conditionally display based on data availability.
This commit is contained in:
AmirReza Jamali
2026-04-15 15:57:36 +03:30
parent 2e3b721585
commit c1b6b4ccf1
16 changed files with 149 additions and 65 deletions
+6
View File
@@ -6,6 +6,7 @@ interface EmptyListWrapperProps<T> {
emptyMessage: string;
columns: string[];
children: ReactNode;
isLoading?: boolean;
}
function EmptyListWrapper<T>({
@@ -13,7 +14,12 @@ function EmptyListWrapper<T>({
emptyMessage,
columns,
children,
isLoading = false,
}: EmptyListWrapperProps<T>) {
if (isLoading) {
return null;
}
if (!list.length) {
return (
<TableRow>
+2 -1
View File
@@ -17,6 +17,7 @@ import {
import TableSkeleton from "@/components/ui/TableSkeleton";
import { AdminStatus } from "@/constants/enums";
import { _TooltipDefaultParams } from "@/constants/tooltip";
import { getPaginatedRowNumber } from "@/utils/shared";
import { Tooltip } from "react-tooltip";
import AdminListFilters from "./AdminListFilters";
import ChangeStatusModalContent from "./ChangeStatusModalContent";
@@ -77,7 +78,7 @@ const AdminsTable = () => {
className="odd:bg-gray-2 dark:odd:bg-gray-7"
>
<TableCell className="text-start" colSpan={100}>
{index + 1}
{getPaginatedRowNumber({ index })}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item?.full_name}
+6 -2
View File
@@ -7,7 +7,7 @@ import Modal from "@/components/ui/modal";
import { Table, TableBody, TableCell, TableHead } from "@/components/ui/table";
import TableSkeleton from "@/components/ui/TableSkeleton";
import { _TooltipDefaultParams } from "@/constants/tooltip";
import { unixToDate } from "@/utils/shared";
import { getPaginatedRowNumber, unixToDate } from "@/utils/shared";
import { Tooltip } from "react-tooltip";
import { TableHeader, TableRow } from "../../ui/table";
import CmsListFilters from "./CmsListFilters";
@@ -60,7 +60,11 @@ const CmsTable = () => {
className="odd:bg-gray-2 dark:odd:bg-gray-7"
>
<TableCell className="text-start" colSpan={100}>
{index + 1}
{getPaginatedRowNumber({
index,
page: data?.meta?.page,
limit: data?.meta?.limit,
})}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item.key}
@@ -14,7 +14,7 @@ import {
} from "@/components/ui/table";
import TableSkeleton from "@/components/ui/TableSkeleton";
import { _TooltipDefaultParams } from "@/constants/tooltip";
import { unixToDate } from "@/utils/shared";
import { getPaginatedRowNumber, unixToDate } from "@/utils/shared";
import { Tooltip } from "react-tooltip";
import { Skeleton } from "../../../ui/skeleton";
import useCompanyCategoriesPresenter from "./useCompanyCategoriesPresenter";
@@ -83,7 +83,9 @@ const CompanyCategoriesTable = () => {
key={category.id}
className="odd:bg-gray-2 dark:odd:bg-gray-7"
>
<TableCell colSpan={100}>{index + 1}</TableCell>
<TableCell colSpan={100}>
{getPaginatedRowNumber({ index })}
</TableCell>
<TableCell colSpan={100}>{category.name}</TableCell>
<TableCell colSpan={100}>{category.description}</TableCell>
<TableCell colSpan={100}>
+2 -2
View File
@@ -17,7 +17,7 @@ import {
} from "@/components/ui/table";
import TableSkeleton from "@/components/ui/TableSkeleton";
import { _TooltipDefaultParams } from "@/constants/tooltip";
import { formatPhoneNumber } from "@/utils/shared";
import { formatPhoneNumber, getPaginatedRowNumber } from "@/utils/shared";
import { Tooltip } from "react-tooltip";
import { useCompanyListPresenter } from "./useCompanyListPresenter";
import ListHeader from "@/components/ui/ListHeader";
@@ -81,7 +81,7 @@ const CompaniesTable = () => {
className="odd:bg-gray-2 dark:odd:bg-gray-7"
>
<TableCell className="text-start" colSpan={100}>
{index + 1}
{getPaginatedRowNumber({ index })}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{company.name}
+2 -2
View File
@@ -12,7 +12,7 @@ import {
} from "@/components/ui/table";
import TableSkeleton from "@/components/ui/TableSkeleton";
import { _TooltipDefaultParams } from "@/constants/tooltip";
import { unixToDate } from "@/utils/shared";
import { getPaginatedRowNumber, unixToDate } from "@/utils/shared";
import { Tooltip } from "react-tooltip";
import { useContactUsListPresenter } from "./useContactUsListPresenter";
@@ -58,7 +58,7 @@ const ContactUsTable = () => {
className="odd:bg-gray-2 dark:odd:bg-gray-7"
>
<TableCell className="text-start" colSpan={100}>
{index + 1}
{getPaginatedRowNumber({ index })}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item?.full_name}
+25 -13
View File
@@ -3,6 +3,7 @@ import { ExclamationIcon, PencilSquareIcon, TrashIcon } from "@/assets/icons";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import { Building } from "@/components/Layouts/sidebar/icons";
import ConfirmationModal from "@/components/ui/ConfirmationModal";
import IsVisible from "@/components/ui/IsVisible";
import ListHeader from "@/components/ui/ListHeader";
import Modal from "@/components/ui/modal";
import Pagination from "@/components/ui/pagination";
@@ -17,7 +18,7 @@ import {
} from "@/components/ui/table";
import TableSkeleton from "@/components/ui/TableSkeleton";
import { _TooltipDefaultParams } from "@/constants/tooltip";
import { unixToDate } from "@/utils/shared";
import { getPaginatedRowNumber, unixToDate } from "@/utils/shared";
import { toast } from "react-toastify";
import { Tooltip } from "react-tooltip";
import AssignToCompanyModalContent from "./AssignToCompanyModalContent";
@@ -52,7 +53,9 @@ const CustomersTable = () => {
isMutating,
setParams,
assignCompanyCompaniesQuery,
shouldShowPagination,
} = useCustomerListPresenter();
return (
<div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
<ListHeader
@@ -79,10 +82,17 @@ const CustomersTable = () => {
list={allCustomers ?? []}
columns={columns}
emptyMessage="No Customers were found"
isLoading={isPending}
>
{allCustomers?.map((customer, index) => (
<TableRow key={customer?.id ?? index}>
<TableCell colSpan={100}>{index + 1}</TableCell>
<TableCell colSpan={100}>
{getPaginatedRowNumber({
index,
page: metadata?.page,
limit: metadata?.limit,
})}
</TableCell>
<TableCell colSpan={100}>{customer?.full_name}</TableCell>
<TableCell colSpan={100}>{customer?.email}</TableCell>
<TableCell colSpan={100}>
@@ -174,17 +184,19 @@ const CustomersTable = () => {
</EmptyListWrapper>
</TableBody>
</Table>
<Pagination
currentPage={metadata?.page ? metadata?.page - 1 : 0}
totalPages={metadata?.pages ?? 1}
onPageChange={(e) => {
setParams((currentParams) => ({
...currentParams,
offset: e.selected * (metadata?.limit ?? 10),
limit: metadata?.limit ?? 10,
}));
}}
/>
<IsVisible condition={shouldShowPagination}>
<Pagination
currentPage={metadata?.page ? metadata?.page - 1 : 0}
totalPages={metadata?.pages ?? 1}
onPageChange={(e) => {
setParams((currentParams) => ({
...currentParams,
offset: e.selected * (metadata?.limit ?? 10),
limit: metadata?.limit ?? 10,
}));
}}
/>
</IsVisible>
<Modal
isOpen={isFilterModalOpen}
onClose={() => setIsFilterModalOpen(false)}
@@ -82,6 +82,9 @@ const useCustomerListPresenter = () => {
};
const customers = data?.data?.customers ?? [];
const shouldShowPagination =
!isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
return {
allCustomers: customers,
metadata: data?.meta,
@@ -109,6 +112,7 @@ const useCustomerListPresenter = () => {
isMutating,
setParams,
assignCompanyCompaniesQuery,
shouldShowPagination,
};
};
@@ -10,7 +10,7 @@ import {
} from "@/components/ui/table";
import TableSkeleton from "@/components/ui/TableSkeleton";
import { cn } from "@/lib/utils";
import { capitalize, unixToDate } from "@/utils/shared";
import { capitalize, getPaginatedRowNumber, unixToDate } from "@/utils/shared";
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
import useFeedbackTablePresenter from "./useFeedbackTablePresenter";
@@ -60,7 +60,7 @@ const FeedbackTable = ({ id, paramKey }: IProps) => {
className="odd:bg-gray-2 dark:odd:bg-gray-7"
>
<TableCell className="text-start" colSpan={100}>
{index + 1}
{getPaginatedRowNumber({ index })}
</TableCell>
<TableCell className="uppercase" colSpan={100}>
{item.feedback_type}
+25 -15
View File
@@ -2,6 +2,7 @@
import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import ConfirmationModal from "@/components/ui/ConfirmationModal";
import IsVisible from "@/components/ui/IsVisible";
import ListHeader from "@/components/ui/ListHeader";
import Modal from "@/components/ui/modal";
import Pagination from "@/components/ui/pagination";
@@ -16,7 +17,7 @@ import {
} from "@/components/ui/table";
import TableSkeleton from "@/components/ui/TableSkeleton";
import { _TooltipDefaultParams } from "@/constants/tooltip";
import { formatPhoneNumber, unixToDate } from "@/utils/shared";
import { formatPhoneNumber, getPaginatedRowNumber, unixToDate } from "@/utils/shared";
import { Tooltip } from "react-tooltip";
import ChangeStatusForm from "./ChangeStatusForm";
import InquiriesListFilters from "./InquiriesListFilters";
@@ -45,7 +46,9 @@ const InquiriesTable = () => {
setFilterValue,
isMutating,
setParams,
shouldShowPagination,
} = useInquiriesListPresenter();
return (
<div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
<ListHeader
@@ -68,11 +71,16 @@ const InquiriesTable = () => {
columns={columns}
list={inquiries ?? []}
emptyMessage="No Inquiries were found"
isLoading={isPending}
>
{inquiries?.map((inquiry, index) => (
<TableRow key={inquiry?.id ?? index}>
<TableCell className="text-start" colSpan={100}>
{index + 1}
{getPaginatedRowNumber({
index,
page: allInquiries?.meta?.page,
limit: allInquiries?.meta?.limit,
})}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{inquiry?.company_name}
@@ -146,19 +154,21 @@ const InquiriesTable = () => {
watch={watch}
/>
</Modal>
<Pagination
currentPage={
allInquiries?.meta?.page ? allInquiries?.meta?.page - 1 : 0
}
totalPages={allInquiries?.meta?.pages ?? 1}
onPageChange={(e) => {
setParams((currentParams) => ({
...currentParams,
offset: e.selected * (allInquiries?.meta?.limit ?? 10),
limit: allInquiries?.meta?.limit ?? 10,
}));
}}
/>
<IsVisible condition={shouldShowPagination}>
<Pagination
currentPage={
allInquiries?.meta?.page ? allInquiries?.meta?.page - 1 : 0
}
totalPages={allInquiries?.meta?.pages ?? 1}
onPageChange={(e) => {
setParams((currentParams) => ({
...currentParams,
offset: e.selected * (allInquiries?.meta?.limit ?? 10),
limit: allInquiries?.meta?.limit ?? 10,
}));
}}
/>
</IsVisible>
<Modal
isOpen={isChangeStatusModalOpen}
onClose={() => setIsChangeStatusModalOpen(false)}
@@ -52,6 +52,8 @@ const useInquiriesListPresenter = () => {
}, [searchParams, filterFormReset]);
const { data, isPending } = useGetInquiries(params);
const shouldShowPagination =
!isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
const { mutate } = useDeleteInquiry(() => setIsDeleteModalOpen(false));
const { mutate: changeStatus } =
useChangeInquiryStatus(() => setIsChangeStatusModalOpen(false));
@@ -100,6 +102,7 @@ const useInquiriesListPresenter = () => {
setFilterValue,
isMutating,
setParams,
shouldShowPagination,
};
};
@@ -2,6 +2,7 @@
import { EyeIcon } from "@/assets/icons";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
import IsVisible from "@/components/ui/IsVisible";
import Pagination from "@/components/ui/pagination";
import Status from "@/components/ui/Status";
import {
@@ -14,7 +15,7 @@ import {
} from "@/components/ui/table";
import TableSkeleton from "@/components/ui/TableSkeleton";
import { _TooltipDefaultParams } from "@/constants/tooltip";
import { truncateString, unixToDate } from "@/utils/shared";
import { getPaginatedRowNumber, truncateString, unixToDate } from "@/utils/shared";
import { Tooltip } from "react-tooltip";
import Boolean from "@/components/ui/Boolean";
@@ -40,6 +41,7 @@ const NotificationHistoryTable = () => {
watch,
search,
isMutating,
shouldShowPagination,
} = useNotificationHistoryTablePresenter();
return (
@@ -65,10 +67,17 @@ const NotificationHistoryTable = () => {
list={notificationHistory ?? []}
columns={columns}
emptyMessage="No notifications were found"
isLoading={isLoading}
>
{(notificationHistory ?? []).map((item, index) => (
<TableRow key={item.id}>
<TableCell colSpan={100}>{index + 1}</TableCell>
<TableCell colSpan={100}>
{getPaginatedRowNumber({
index,
page: metadata?.page,
limit: metadata?.limit,
})}
</TableCell>
<TableCell colSpan={100}>{item.title}</TableCell>
<TableCell colSpan={100}>{item.event_type}</TableCell>
<TableCell colSpan={100}>
@@ -117,17 +126,19 @@ const NotificationHistoryTable = () => {
</EmptyListWrapper>
</TableBody>
</Table>
<Pagination
currentPage={metadata?.page ? metadata?.page - 1 : 0}
totalPages={metadata?.pages ?? 1}
onPageChange={(e) => {
setParams((currentParams) => ({
...currentParams,
offset: e.selected * (metadata?.limit ?? 10),
limit: metadata?.limit ?? 10,
}));
}}
/>
<IsVisible condition={shouldShowPagination}>
<Pagination
currentPage={metadata?.page ? metadata?.page - 1 : 0}
totalPages={metadata?.pages ?? 1}
onPageChange={(e) => {
setParams((currentParams) => ({
...currentParams,
offset: e.selected * (metadata?.limit ?? 10),
limit: metadata?.limit ?? 10,
}));
}}
/>
</IsVisible>
</ShowcaseSection>
<Modal
isOpen={isFilterModalOpen}
@@ -14,6 +14,8 @@ const useNotificationHistoryTablePresenter = () => {
...apiDefaultParams,
});
const { data, isLoading } = useGetNotificationHistoryQuery(params);
const shouldShowPagination =
!isLoading && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
const isMutating = useIsMutating();
const columns: string[] = [
"row",
@@ -54,6 +56,7 @@ const useNotificationHistoryTablePresenter = () => {
setIsFilterModalOpen,
isMutating,
search,
shouldShowPagination,
};
};
+22 -12
View File
@@ -15,12 +15,14 @@ import useTenderListPresenter from "./useTenderListPresenter";
import { ExclamationIcon, EyeIcon } from "@/assets/icons";
import IsVisible from "@/components/ui/IsVisible";
import ListHeader from "@/components/ui/ListHeader";
import Modal from "@/components/ui/modal";
import Pagination from "@/components/ui/pagination";
import TableSkeleton from "@/components/ui/TableSkeleton";
import { _TooltipDefaultParams } from "@/constants/tooltip";
import { cn } from "@/lib/utils";
import { getPaginatedRowNumber } from "@/utils/shared";
import Link from "next/link";
import TenderListFilters from "./TenderListFilters";
const TendersTable = () => {
@@ -39,6 +41,7 @@ const TendersTable = () => {
watch,
setFilterValue,
isMutating,
shouldShowPagination,
} = useTenderListPresenter();
return (
@@ -64,6 +67,7 @@ const TendersTable = () => {
list={allTenders?.data.tenders ?? []}
columns={columns}
emptyMessage="No tenders were found"
isLoading={isPending}
>
{(allTenders?.data.tenders ?? []).map((item, index) => (
<TableRow
@@ -71,7 +75,11 @@ const TendersTable = () => {
className="odd:bg-gray-2 dark:odd:bg-gray-7"
>
<TableCell className="text-start" colSpan={100}>
{index + 1}
{getPaginatedRowNumber({
index,
page: allTenders?.meta?.page,
limit: allTenders?.meta?.limit,
})}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item.title}
@@ -145,17 +153,19 @@ const TendersTable = () => {
watch={watch}
/>
</Modal>
<Pagination
currentPage={allTenders?.meta?.page ? allTenders?.meta?.page - 1 : 0}
totalPages={allTenders?.meta?.pages ?? 1}
onPageChange={(e) => {
setParams((currentParams) => ({
...currentParams,
offset: e.selected * (allTenders?.meta?.limit ?? 10),
limit: allTenders?.meta?.limit ?? 10,
}));
}}
/>
<IsVisible condition={shouldShowPagination}>
<Pagination
currentPage={allTenders?.meta?.page ? allTenders?.meta?.page - 1 : 0}
totalPages={allTenders?.meta?.pages ?? 1}
onPageChange={(e) => {
setParams((currentParams) => ({
...currentParams,
offset: e.selected * (allTenders?.meta?.limit ?? 10),
limit: allTenders?.meta?.limit ?? 10,
}));
}}
/>
</IsVisible>
</div>
);
};
@@ -43,6 +43,8 @@ const useTenderListPresenter = () => {
}, [searchParams, filterFormReset]);
const { data, error, isPending } = useTendersQuery(params);
const shouldShowPagination =
!isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
const search = (data: any) => {
const newParams = { ...params, ...data, offset: 0 };
@@ -82,6 +84,7 @@ const useTenderListPresenter = () => {
watch,
setFilterValue,
isMutating,
shouldShowPagination,
};
};
+16 -1
View File
@@ -110,4 +110,19 @@ export const deleteEmptyKeys = (obj: Record<string, any>) => {
}
return newObj;
};
export const isValidAlpha2CountryCode = (code: string) => /^[A-Za-z]{2}$/.test(code);
export const isValidAlpha2CountryCode = (code: string) => /^[A-Za-z]{2}$/.test(code);
export const getPaginatedRowNumber = ({
index,
page = 1,
limit = 10,
}: {
index: number;
page?: number;
limit?: number;
}) => {
const safePage = page > 0 ? page : 1;
const safeLimit = limit > 0 ? limit : 10;
return (safePage - 1) * safeLimit + index + 1;
};