feat(contact-us & feedback tables): implement pagination and loading states

- Added pagination functionality to ContactUsTable and FeedbackTable components, enhancing user navigation through large datasets.
- Integrated loading states to provide visual feedback during data fetching.
- Updated data fetching hooks to accept parameters for improved query handling.
- Refactored table row rendering to include pagination metadata for accurate row numbering.
- Enhanced overall user experience by conditionally rendering pagination controls based on data availability.
This commit is contained in:
AmirReza Jamali
2026-05-11 17:30:34 +03:30
parent 71f961ee49
commit 88bc939956
8 changed files with 129 additions and 22 deletions
+24 -1
View File
@@ -2,7 +2,9 @@
import { TrashIcon } from "@/assets/icons"; import { TrashIcon } from "@/assets/icons";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper"; import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import ConfirmationModal from "@/components/ui/ConfirmationModal"; import ConfirmationModal from "@/components/ui/ConfirmationModal";
import IsVisible from "@/components/ui/IsVisible";
import ListWrapper from "@/components/ui/ListWrapper"; import ListWrapper from "@/components/ui/ListWrapper";
import Pagination from "@/components/ui/pagination";
import { import {
Table, Table,
TableBody, TableBody,
@@ -27,6 +29,9 @@ const ContactUsTable = () => {
setCurrentContact, setCurrentContact,
isModalOpen, isModalOpen,
currentContact, currentContact,
metadata,
setParams,
shouldShowPagination,
} = useContactUsListPresenter(); } = useContactUsListPresenter();
return ( return (
<ListWrapper> <ListWrapper>
@@ -50,6 +55,7 @@ const ContactUsTable = () => {
list={contacts ?? []} list={contacts ?? []}
columns={columns} columns={columns}
emptyMessage="No contact us is submitted" emptyMessage="No contact us is submitted"
isLoading={isPending}
> >
{contacts?.map((item, index) => ( {contacts?.map((item, index) => (
<TableRow <TableRow
@@ -57,7 +63,11 @@ const ContactUsTable = () => {
className="odd:bg-gray-2 dark:odd:bg-gray-7" className="odd:bg-gray-2 dark:odd:bg-gray-7"
> >
<TableCell className="text-start" colSpan={100}> <TableCell className="text-start" colSpan={100}>
{getPaginatedRowNumber({ index })} {getPaginatedRowNumber({
index,
page: metadata?.page,
limit: metadata?.limit,
})}
</TableCell> </TableCell>
<TableCell className="text-start" colSpan={100}> <TableCell className="text-start" colSpan={100}>
{item?.full_name} {item?.full_name}
@@ -112,6 +122,19 @@ const ContactUsTable = () => {
onCancel={() => setIsModalOpen(false)} onCancel={() => setIsModalOpen(false)}
/> />
)} )}
<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>
</ListWrapper> </ListWrapper>
); );
}; };
@@ -1,14 +1,17 @@
"use client"; "use client";
import { apiDefaultParams } from "@/constants/Api_Params";
import { import {
useDeleteContactUs, useDeleteContactUs,
useReadAllContactUs, useReadAllContactUs,
} from "@/hooks/queries/useContactUsQuery"; } from "@/hooks/queries/useContactUsQuery";
import { TContact } from "@/lib/api/types/TContacts"; import { TContact } from "@/lib/api/types/TContacts";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useState } from "react"; import { useEffect, useState } from "react";
export const useContactUsListPresenter = () => { export const useContactUsListPresenter = () => {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [currentContact, setCurrentContact] = useState<TContact>(); const [currentContact, setCurrentContact] = useState<TContact>();
const [params, setParams] = useState<Record<string, unknown>>({});
const columns: string[] = [ const columns: string[] = [
"row", "row",
"Full name", "Full name",
@@ -20,11 +23,28 @@ export const useContactUsListPresenter = () => {
]; ];
const pathname = usePathname(); const pathname = usePathname();
const router = useRouter(); const router = useRouter();
const { data, isPending } = useReadAllContactUs(); const searchParams = useSearchParams();
useEffect(() => {
const newParams: Record<string, unknown> = {
...apiDefaultParams,
offset: 0,
limit: 10,
};
for (const [key, value] of searchParams.entries()) {
newParams[key] = value;
}
setParams(newParams);
}, [searchParams]);
const { data, isPending } = useReadAllContactUs(params);
const { mutate: deleteContactUS } = useDeleteContactUs(() => const { mutate: deleteContactUS } = useDeleteContactUs(() =>
setIsModalOpen(false), setIsModalOpen(false),
); );
const shouldShowPagination =
!isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
const onConfirmDelete = () => { const onConfirmDelete = () => {
if (!currentContact) return; if (!currentContact) return;
deleteContactUS(currentContact?.id); deleteContactUS(currentContact?.id);
@@ -35,6 +55,9 @@ export const useContactUsListPresenter = () => {
router, router,
isPending, isPending,
contacts: data?.data.contacts, contacts: data?.data.contacts,
metadata: data?.meta,
setParams,
shouldShowPagination,
onConfirmDelete, onConfirmDelete,
setIsModalOpen, setIsModalOpen,
setCurrentContact, setCurrentContact,
+27 -3
View File
@@ -1,5 +1,9 @@
"use client";
import { ExclamationIcon } from "@/assets/icons"; import { ExclamationIcon } from "@/assets/icons";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper"; 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 { import {
Table, Table,
TableBody, TableBody,
@@ -11,9 +15,8 @@ import {
import TableSkeleton from "@/components/ui/TableSkeleton"; import TableSkeleton from "@/components/ui/TableSkeleton";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { capitalize, getPaginatedRowNumber, unixToDate } from "@/utils/shared"; import { capitalize, getPaginatedRowNumber, unixToDate } from "@/utils/shared";
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
import useFeedbackTablePresenter from "./useFeedbackTablePresenter"; import useFeedbackTablePresenter from "./useFeedbackTablePresenter";
interface IProps { interface IProps {
id: string; id: string;
paramKey: "tender_id" | "company_id" | "customer_id"; paramKey: "tender_id" | "company_id" | "customer_id";
@@ -26,6 +29,9 @@ const FeedbackTable = ({ id, paramKey }: IProps) => {
pathName, pathName,
router, router,
getShowcaseSectionTitle, getShowcaseSectionTitle,
metadata,
setParams,
shouldShowPagination,
} = useFeedbackTablePresenter({ id, paramKey }); } = useFeedbackTablePresenter({ id, paramKey });
return ( return (
@@ -53,6 +59,7 @@ const FeedbackTable = ({ id, paramKey }: IProps) => {
list={feedback ?? []} list={feedback ?? []}
columns={columns} columns={columns}
emptyMessage="No feedback were found" emptyMessage="No feedback were found"
isLoading={isLoading}
> >
{(feedback ?? []).map((item, index) => ( {(feedback ?? []).map((item, index) => (
<TableRow <TableRow
@@ -60,7 +67,11 @@ const FeedbackTable = ({ id, paramKey }: IProps) => {
className="odd:bg-gray-2 dark:odd:bg-gray-7" className="odd:bg-gray-2 dark:odd:bg-gray-7"
> >
<TableCell className="text-start" colSpan={100}> <TableCell className="text-start" colSpan={100}>
{getPaginatedRowNumber({ index })} {getPaginatedRowNumber({
index,
page: metadata?.page,
limit: metadata?.limit,
})}
</TableCell> </TableCell>
<TableCell className="uppercase" colSpan={100}> <TableCell className="uppercase" colSpan={100}>
{item.feedback_type} {item.feedback_type}
@@ -90,6 +101,19 @@ const FeedbackTable = ({ id, paramKey }: IProps) => {
</EmptyListWrapper> </EmptyListWrapper>
</TableBody> </TableBody>
</Table> </Table>
<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> </ShowcaseSection>
); );
}; };
@@ -1,14 +1,38 @@
"use client";
import { apiDefaultParams } from "@/constants/Api_Params";
import { useGetFeedback } from "@/hooks/queries"; import { useGetFeedback } from "@/hooks/queries";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
interface IProps { interface IProps {
id: string; id: string;
paramKey: "tender_id" | "company_id" | "customer_id"; paramKey: "tender_id" | "company_id" | "customer_id";
} }
const useFeedbackTablePresenter = ({ id, paramKey }: IProps) => { const useFeedbackTablePresenter = ({ id, paramKey }: IProps) => {
const { data, isLoading } = useGetFeedback({ [paramKey]: id }); const [params, setParams] = useState<Record<string, unknown>>({});
const searchParams = useSearchParams();
const router = useRouter(); const router = useRouter();
const pathName = usePathname(); const pathName = usePathname();
useEffect(() => {
const newParams: Record<string, unknown> = {
...apiDefaultParams,
offset: 0,
limit: 10,
};
for (const [key, value] of searchParams.entries()) {
newParams[key] = value;
}
setParams(newParams);
}, [searchParams]);
const queryParams = useMemo(
() => ({ ...params, [paramKey]: id }),
[params, paramKey, id],
);
const { data, isLoading } = useGetFeedback(queryParams);
const columns = [ const columns = [
"row", "row",
"feedback type", "feedback type",
@@ -16,12 +40,17 @@ const useFeedbackTablePresenter = ({ id, paramKey }: IProps) => {
"created at", "created at",
"actions", "actions",
]; ];
const shouldShowPagination =
!isLoading &&
(data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
const getShowcaseSectionTitle = () => { const getShowcaseSectionTitle = () => {
if (paramKey === "tender_id") { if (paramKey === "tender_id") {
return `: ${data?.data?.[0]?.tender?.title}`; const title = data?.data?.[0]?.tender?.title;
} else { return title ? `: ${title}` : "";
return "";
} }
return "";
}; };
return { return {
isLoading, isLoading,
@@ -30,6 +59,9 @@ const useFeedbackTablePresenter = ({ id, paramKey }: IProps) => {
pathName, pathName,
router, router,
getShowcaseSectionTitle, getShowcaseSectionTitle,
metadata: data?.meta,
setParams,
shouldShowPagination,
}; };
}; };
+6 -3
View File
@@ -4,12 +4,15 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMemo } from "react"; import { useMemo } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
export const useReadAllContactUs = () => { export const useReadAllContactUs = (params?: Record<string, unknown>) => {
const queryKey = useMemo(() => [API_ENDPOINTS.CONTACT_US.BASE()], []); const queryKey = useMemo(
() => [API_ENDPOINTS.CONTACT_US.BASE(), params],
[params],
);
return useQuery({ return useQuery({
queryKey, queryKey,
queryFn: () => contactUsService.getAllContactUs(), queryFn: () => contactUsService.getAllContactUs(params),
}); });
}; };
export const useDeleteContactUs = (successCallback?: () => void) => { export const useDeleteContactUs = (successCallback?: () => void) => {
+1 -1
View File
@@ -10,7 +10,7 @@ export const useGetFeedSingleFeedback = (id: string) => {
queryFn: () => feedbackService.getSingleFeedback(id), queryFn: () => feedbackService.getSingleFeedback(id),
}); });
}; };
export const useGetFeedback = (params: Record<string, any>) => { export const useGetFeedback = (params: Record<string, unknown>) => {
const queryKey = useMemo( const queryKey = useMemo(
() => [API_ENDPOINTS.FEEDBACK.READ_ALL, params], () => [API_ENDPOINTS.FEEDBACK.READ_ALL, params],
[params], [params],
+4 -2
View File
@@ -3,9 +3,11 @@ import { API_ENDPOINTS } from "../endpoints";
import { TContactApiResponseSchema } from "../types/TContacts"; import { TContactApiResponseSchema } from "../types/TContacts";
export const contactUsService = { export const contactUsService = {
getAllContactUs: async (): Promise<TContactApiResponseSchema> => { getAllContactUs: async (
params?: Record<string, unknown>,
): Promise<TContactApiResponseSchema> => {
try { try {
return (await api.get(API_ENDPOINTS.CONTACT_US.BASE())).data; return (await api.get(API_ENDPOINTS.CONTACT_US.BASE(), { params })).data;
} catch (error) { } catch (error) {
console.error( console.error(
"ERROR caught in contact-us service => getAllContactUs", "ERROR caught in contact-us service => getAllContactUs",
+4 -4
View File
@@ -1,12 +1,12 @@
import api from "../axios"; import api from "../axios";
import { API_ENDPOINTS } from "../endpoints"; import { API_ENDPOINTS } from "../endpoints";
import { ApiResponse } from "../types"; import { ApiResponse } from "../types";
import { TFeedbackResponse } from "../types/Feedback"; import { TFeedback } from "../types/Feedback";
export const feedbackService = { export const feedbackService = {
getFeedbacks: async ( getFeedbacks: async (
params: Record<string, any>, params: Record<string, unknown>,
): Promise<TFeedbackResponse> => { ): Promise<ApiResponse<TFeedback[]>> => {
try { try {
return (await api.get(API_ENDPOINTS.FEEDBACK.READ_ALL, { params })).data; return (await api.get(API_ENDPOINTS.FEEDBACK.READ_ALL, { params })).data;
} catch (error) { } catch (error) {
@@ -16,7 +16,7 @@ export const feedbackService = {
}, },
getSingleFeedback: async ( getSingleFeedback: async (
id: string, id: string,
): Promise<ApiResponse<TFeedbackResponse>> => { ): Promise<ApiResponse<TFeedback>> => {
try { try {
return (await api.get(API_ENDPOINTS.FEEDBACK.DETAILS(id))).data; return (await api.get(API_ENDPOINTS.FEEDBACK.DETAILS(id))).data;
} catch (error) { } catch (error) {