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:
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user