From 88bc939956cd05347fdf58af8364005a3b793732 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Mon, 11 May 2026 17:30:34 +0330 Subject: [PATCH] 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. --- src/components/Tables/contact-us/index.tsx | 25 ++++++++++- .../contact-us/useContactUsListPresenter.ts | 29 +++++++++++-- .../Tables/feedback-table/index.tsx | 30 +++++++++++-- .../useFeedbackTablePresenter.ts | 42 ++++++++++++++++--- src/hooks/queries/useContactUsQuery.ts | 9 ++-- src/hooks/queries/useFeedbackQueries.ts | 2 +- src/lib/api/services/contact-us-service.ts | 6 ++- src/lib/api/services/feedback-service.ts | 8 ++-- 8 files changed, 129 insertions(+), 22 deletions(-) diff --git a/src/components/Tables/contact-us/index.tsx b/src/components/Tables/contact-us/index.tsx index 44ccb24..2c6612c 100644 --- a/src/components/Tables/contact-us/index.tsx +++ b/src/components/Tables/contact-us/index.tsx @@ -2,7 +2,9 @@ import { TrashIcon } from "@/assets/icons"; import EmptyListWrapper from "@/components/HOC/EmptyListWrapper"; import ConfirmationModal from "@/components/ui/ConfirmationModal"; +import IsVisible from "@/components/ui/IsVisible"; import ListWrapper from "@/components/ui/ListWrapper"; +import Pagination from "@/components/ui/pagination"; import { Table, TableBody, @@ -27,6 +29,9 @@ const ContactUsTable = () => { setCurrentContact, isModalOpen, currentContact, + metadata, + setParams, + shouldShowPagination, } = useContactUsListPresenter(); return ( @@ -50,6 +55,7 @@ const ContactUsTable = () => { list={contacts ?? []} columns={columns} emptyMessage="No contact us is submitted" + isLoading={isPending} > {contacts?.map((item, index) => ( { className="odd:bg-gray-2 dark:odd:bg-gray-7" > - {getPaginatedRowNumber({ index })} + {getPaginatedRowNumber({ + index, + page: metadata?.page, + limit: metadata?.limit, + })} {item?.full_name} @@ -112,6 +122,19 @@ const ContactUsTable = () => { onCancel={() => setIsModalOpen(false)} /> )} + + { + setParams((currentParams) => ({ + ...currentParams, + offset: e.selected * (metadata?.limit ?? 10), + limit: metadata?.limit ?? 10, + })); + }} + /> + ); }; diff --git a/src/components/Tables/contact-us/useContactUsListPresenter.ts b/src/components/Tables/contact-us/useContactUsListPresenter.ts index d761fcf..c373f5e 100644 --- a/src/components/Tables/contact-us/useContactUsListPresenter.ts +++ b/src/components/Tables/contact-us/useContactUsListPresenter.ts @@ -1,14 +1,17 @@ "use client"; +import { apiDefaultParams } from "@/constants/Api_Params"; import { useDeleteContactUs, useReadAllContactUs, } from "@/hooks/queries/useContactUsQuery"; import { TContact } from "@/lib/api/types/TContacts"; -import { usePathname, useRouter } from "next/navigation"; -import { useState } from "react"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; + export const useContactUsListPresenter = () => { const [isModalOpen, setIsModalOpen] = useState(false); const [currentContact, setCurrentContact] = useState(); + const [params, setParams] = useState>({}); const columns: string[] = [ "row", "Full name", @@ -20,11 +23,28 @@ export const useContactUsListPresenter = () => { ]; const pathname = usePathname(); const router = useRouter(); - const { data, isPending } = useReadAllContactUs(); + const searchParams = useSearchParams(); + + useEffect(() => { + const newParams: Record = { + ...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(() => setIsModalOpen(false), ); + const shouldShowPagination = + !isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10; + const onConfirmDelete = () => { if (!currentContact) return; deleteContactUS(currentContact?.id); @@ -35,6 +55,9 @@ export const useContactUsListPresenter = () => { router, isPending, contacts: data?.data.contacts, + metadata: data?.meta, + setParams, + shouldShowPagination, onConfirmDelete, setIsModalOpen, setCurrentContact, diff --git a/src/components/Tables/feedback-table/index.tsx b/src/components/Tables/feedback-table/index.tsx index 3196ce8..e076a10 100644 --- a/src/components/Tables/feedback-table/index.tsx +++ b/src/components/Tables/feedback-table/index.tsx @@ -1,5 +1,9 @@ +"use client"; import { ExclamationIcon } 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 { Table, TableBody, @@ -11,9 +15,8 @@ import { import TableSkeleton from "@/components/ui/TableSkeleton"; import { cn } from "@/lib/utils"; import { capitalize, getPaginatedRowNumber, unixToDate } from "@/utils/shared"; - -import { ShowcaseSection } from "@/components/Layouts/showcase-section"; import useFeedbackTablePresenter from "./useFeedbackTablePresenter"; + interface IProps { id: string; paramKey: "tender_id" | "company_id" | "customer_id"; @@ -26,6 +29,9 @@ const FeedbackTable = ({ id, paramKey }: IProps) => { pathName, router, getShowcaseSectionTitle, + metadata, + setParams, + shouldShowPagination, } = useFeedbackTablePresenter({ id, paramKey }); return ( @@ -53,6 +59,7 @@ const FeedbackTable = ({ id, paramKey }: IProps) => { list={feedback ?? []} columns={columns} emptyMessage="No feedback were found" + isLoading={isLoading} > {(feedback ?? []).map((item, index) => ( { className="odd:bg-gray-2 dark:odd:bg-gray-7" > - {getPaginatedRowNumber({ index })} + {getPaginatedRowNumber({ + index, + page: metadata?.page, + limit: metadata?.limit, + })} {item.feedback_type} @@ -90,6 +101,19 @@ const FeedbackTable = ({ id, paramKey }: IProps) => { + + { + setParams((currentParams) => ({ + ...currentParams, + offset: e.selected * (metadata?.limit ?? 10), + limit: metadata?.limit ?? 10, + })); + }} + /> + ); }; diff --git a/src/components/Tables/feedback-table/useFeedbackTablePresenter.ts b/src/components/Tables/feedback-table/useFeedbackTablePresenter.ts index 4db3a17..3e375da 100644 --- a/src/components/Tables/feedback-table/useFeedbackTablePresenter.ts +++ b/src/components/Tables/feedback-table/useFeedbackTablePresenter.ts @@ -1,14 +1,38 @@ +"use client"; +import { apiDefaultParams } from "@/constants/Api_Params"; 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 { id: string; paramKey: "tender_id" | "company_id" | "customer_id"; } + const useFeedbackTablePresenter = ({ id, paramKey }: IProps) => { - const { data, isLoading } = useGetFeedback({ [paramKey]: id }); + const [params, setParams] = useState>({}); + const searchParams = useSearchParams(); const router = useRouter(); const pathName = usePathname(); + + useEffect(() => { + const newParams: Record = { + ...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 = [ "row", "feedback type", @@ -16,12 +40,17 @@ const useFeedbackTablePresenter = ({ id, paramKey }: IProps) => { "created at", "actions", ]; + + const shouldShowPagination = + !isLoading && + (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10; + const getShowcaseSectionTitle = () => { if (paramKey === "tender_id") { - return `: ${data?.data?.[0]?.tender?.title}`; - } else { - return ""; + const title = data?.data?.[0]?.tender?.title; + return title ? `: ${title}` : ""; } + return ""; }; return { isLoading, @@ -30,6 +59,9 @@ const useFeedbackTablePresenter = ({ id, paramKey }: IProps) => { pathName, router, getShowcaseSectionTitle, + metadata: data?.meta, + setParams, + shouldShowPagination, }; }; diff --git a/src/hooks/queries/useContactUsQuery.ts b/src/hooks/queries/useContactUsQuery.ts index 317269e..4c022eb 100644 --- a/src/hooks/queries/useContactUsQuery.ts +++ b/src/hooks/queries/useContactUsQuery.ts @@ -4,12 +4,15 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMemo } from "react"; import { toast } from "react-toastify"; -export const useReadAllContactUs = () => { - const queryKey = useMemo(() => [API_ENDPOINTS.CONTACT_US.BASE()], []); +export const useReadAllContactUs = (params?: Record) => { + const queryKey = useMemo( + () => [API_ENDPOINTS.CONTACT_US.BASE(), params], + [params], + ); return useQuery({ queryKey, - queryFn: () => contactUsService.getAllContactUs(), + queryFn: () => contactUsService.getAllContactUs(params), }); }; export const useDeleteContactUs = (successCallback?: () => void) => { diff --git a/src/hooks/queries/useFeedbackQueries.ts b/src/hooks/queries/useFeedbackQueries.ts index 31dcec0..bd01ca0 100644 --- a/src/hooks/queries/useFeedbackQueries.ts +++ b/src/hooks/queries/useFeedbackQueries.ts @@ -10,7 +10,7 @@ export const useGetFeedSingleFeedback = (id: string) => { queryFn: () => feedbackService.getSingleFeedback(id), }); }; -export const useGetFeedback = (params: Record) => { +export const useGetFeedback = (params: Record) => { const queryKey = useMemo( () => [API_ENDPOINTS.FEEDBACK.READ_ALL, params], [params], diff --git a/src/lib/api/services/contact-us-service.ts b/src/lib/api/services/contact-us-service.ts index fd8aec8..0046581 100644 --- a/src/lib/api/services/contact-us-service.ts +++ b/src/lib/api/services/contact-us-service.ts @@ -3,9 +3,11 @@ import { API_ENDPOINTS } from "../endpoints"; import { TContactApiResponseSchema } from "../types/TContacts"; export const contactUsService = { - getAllContactUs: async (): Promise => { + getAllContactUs: async ( + params?: Record, + ): Promise => { try { - return (await api.get(API_ENDPOINTS.CONTACT_US.BASE())).data; + return (await api.get(API_ENDPOINTS.CONTACT_US.BASE(), { params })).data; } catch (error) { console.error( "ERROR caught in contact-us service => getAllContactUs", diff --git a/src/lib/api/services/feedback-service.ts b/src/lib/api/services/feedback-service.ts index 8319194..42d99c6 100644 --- a/src/lib/api/services/feedback-service.ts +++ b/src/lib/api/services/feedback-service.ts @@ -1,12 +1,12 @@ import api from "../axios"; import { API_ENDPOINTS } from "../endpoints"; import { ApiResponse } from "../types"; -import { TFeedbackResponse } from "../types/Feedback"; +import { TFeedback } from "../types/Feedback"; export const feedbackService = { getFeedbacks: async ( - params: Record, - ): Promise => { + params: Record, + ): Promise> => { try { return (await api.get(API_ENDPOINTS.FEEDBACK.READ_ALL, { params })).data; } catch (error) { @@ -16,7 +16,7 @@ export const feedbackService = { }, getSingleFeedback: async ( id: string, - ): Promise> => { + ): Promise> => { try { return (await api.get(API_ENDPOINTS.FEEDBACK.DETAILS(id))).data; } catch (error) {