feat(submissions): Implement submissions page with paginated table

This commit introduces a new page for users to view their submissions in a structured and paginated table. This allows users to easily track their submission history and access key details.

The main changes include:
- A new page at `/submissions` to display the submissions list.
- A `SubmissionsTable` component to render submission data, including title, country, status, and a link to the submission URL.
- Integration of `react-paginate` to handle navigation through large sets of data.
- A new `useSubmissions` custom hook using TanStack Query for efficient, paginated data fetching from the API.
- Addition of the `currency-symbol-map` dependency to display prices with the correct currency symbol.
This commit is contained in:
AmirReza Jamali
2025-09-18 16:52:46 +03:30
parent 2f2114eba9
commit e1898bf259
24 changed files with 411 additions and 86 deletions
+2 -1
View File
@@ -12,6 +12,7 @@ import {
} from "@/components/ui/table";
import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
import ConfirmationModal from "@/components/ui/ConfirmationModal";
import { formatPhoneNumber } from "@/utils/shared";
interface IProps {}
@@ -65,7 +66,7 @@ const CompaniesTable = ({}: IProps) => {
{company.email}
</TableHead>
<TableHead className="text-start" colSpan={100}>
{company.phone}
{formatPhoneNumber(company.phone)}
</TableHead>
<TableHead className="text-start" colSpan={100}>
{company.address.country}
+12 -3
View File
@@ -18,6 +18,9 @@ import Modal from "@/components/ui/modal";
import { TAssignCustomerToCompanyCredentials } from "@/lib/api";
import { toast } from "react-toastify";
import Status from "@/components/ui/Status";
import { unixToDate } from "@/utils/shared";
import { custom } from "zod";
const CustomersTable = () => {
const {
isPending,
@@ -62,10 +65,15 @@ const CustomersTable = () => {
<TableRow key={customer.id}>
<TableCell colSpan={100}>{customer.full_name}</TableCell>
<TableCell colSpan={100}>{customer.email}</TableCell>
<TableCell colSpan={100}>{customer.created_at}</TableCell>
<TableCell colSpan={100}>{customer.type}</TableCell>
<TableCell colSpan={100}>
<Status status={customer.status}>{customer.status}</Status>
{unixToDate(customer.created_at)}
</TableCell>
<TableCell colSpan={100} className="uppercase">{customer.type}</TableCell>
<TableCell colSpan={100}>
<Status status={customer.status}>
<Status status={customer.status}>{customer.status}</Status>
</Status>
</TableCell>
<TableCell colSpan={100}>
{customer.companies?.length ? (
@@ -76,6 +84,7 @@ const CustomersTable = () => {
<span>None</span>
)}
</TableCell>
<TableCell colSpan={100}>{customer.role ?? "-"}</TableCell>
<TableCell className="text-start xl:pr-7.5">
<div className="flex items-center justify-start gap-x-3.5">
<button
@@ -75,6 +75,7 @@ const useCustomerListPresenter = () => {
"type",
"status",
"company",
"role",
"actions",
];
const onConfirmDelete = () => {
+7 -1
View File
@@ -11,7 +11,7 @@ import {
export function TendersSkeleton() {
return (
<TableBody>
{Array.from({ length: 5 }).map((_, i) => (
{Array.from({ length: 20 }).map((_, i) => (
<TableRow key={i}>
<TableCell colSpan={100}>
<Skeleton className="h-8" />
@@ -22,6 +22,12 @@ export function TendersSkeleton() {
<TableCell colSpan={100}>
<Skeleton className="h-8" />
</TableCell>
<TableCell colSpan={100}>
<Skeleton className="h-8" />
</TableCell>
<TableCell colSpan={100}>
<Skeleton className="h-8" />
</TableCell>
</TableRow>
))}
</TableBody>
+49 -25
View File
@@ -14,75 +14,99 @@ import {
import { TendersSkeleton } from "./Skeleton";
import { cn } from "@/lib/utils";
import { getStatusColor } from "@/utils/shared";
import { ExclamationIcon } from "@/assets/icons";
import Pagination from "@/components/ui/pagination";
import Page from "../../../app/forms/form-layout/page";
import Link from "next/link";
const TendersTable = () => {
const {
allTenders,
isPending,
ref,
isFetchingNextPage,
data,
fetchNextPage,
hasNextPage,
} = useTenderListPresenter();
const { allTenders, isPending, router, pathName, setParams } =
useTenderListPresenter();
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">
<Table>
<TableHeader>
<TableRow className="border-none uppercase [&>th]:text-start">
<TableHead colSpan={100} className="text-start font-extrabold">
{/* <TableHead colSpan={100} className="text-start font-extrabold">
Row
</TableHead>
</TableHead> */}
<TableHead colSpan={100} className="text-start font-extrabold">
Title
</TableHead>
<TableHead colSpan={100} className="text-start font-extrabold">
Country Code
</TableHead>
<TableHead colSpan={100} className="text-start font-extrabold">
Submission Url
</TableHead>
<TableHead colSpan={100} className="text-start font-extrabold">
Status
</TableHead>
<TableHead colSpan={100} className="text-start font-extrabold">
Actions
</TableHead>
</TableRow>
</TableHeader>
{isPending && <TendersSkeleton />}
<TableBody>
{allTenders.map((item, index) =>
{allTenders?.data.tenders?.map((item, index) =>
!item ? (
<h6 key={index}>No tenders were found</h6>
) : (
<TableRow
ref={index === allTenders.length - 1 ? ref : null}
key={item.id}
className="odd:bg-gray-2 dark:odd:bg-gray-7"
>
<TableCell className="text-start" colSpan={100}>
{/* <TableCell className="text-start" colSpan={100}>
{index + 1}
</TableCell>
</TableCell> */}
<TableCell className="text-start" colSpan={100}>
{item.title}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item.country_code}
</TableCell>
<TableCell className={cn(`text capitalize`)} colSpan={100}>
{item.submission_url ? (
<Link target="_blank" href={item.submission_url}>
{item.submission_url}
</Link>
) : (
"-"
)}
</TableCell>
<TableCell className={cn(`text capitalize`)} colSpan={100}>
<Status status={item.status}>{item.status}</Status>
</TableCell>
<TableCell className={cn(`text capitalize`)} colSpan={100}>
<button
className="text-gray-40 rounded-full bg-gray-dark bg-opacity-5 hover:text-green-dark dark:bg-gray dark:bg-opacity-5"
onClick={() => {
router.push(`${pathName}/${item.id}`);
}}
>
<ExclamationIcon />
</button>
</TableCell>
</TableRow>
),
)}
</TableBody>
{isFetchingNextPage && <TendersSkeleton />}
</Table>
{(data?.pages.length ?? 0) >= 5 && hasNextPage && (
<button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
className="mt-4 rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700"
>
{isFetchingNextPage ? "Loading..." : "Load More"}
</button>
)}
<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,
}));
}}
/>
</div>
);
};
@@ -1,9 +1,9 @@
"use client";
import { ConfirmationModalProps } from "@/components/ui/ConfirmationModal";
import { useTendersInfiniteQuery } from "@/hooks/queries";
import { useTendersQuery } from "@/hooks/queries";
import useDebounce from "@/hooks/useDebounce";
import { TCustomer } from "@/lib/api";
import { usePathname } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { useInView } from "react-intersection-observer";
import { z } from "zod";
@@ -17,31 +17,18 @@ const useTenderListPresenter = () => {
const [params, setParams] = useState<Record<string, any>>({
sort: "created_at",
});
const router = useRouter();
const debouncedSearch = useDebounce(search, 1000);
const pathName = usePathname();
const {
data,
error,
isPending,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useTendersInfiniteQuery(params);
const { data, error, isPending } = useTendersQuery(params);
const { ref, inView } = useInView({ threshold: 0.5 });
useEffect(() => {
if (inView && hasNextPage && (data?.pages.length ?? 0) < 5) {
fetchNextPage();
}
}, [inView, hasNextPage, fetchNextPage, data?.pages.length]);
useEffect(() => {
const result = z.string().safeParse(debouncedSearch);
if (result.success) {
setParams((prevParams) => ({
...prevParams,
search: debouncedSearch,
}));
}
}, [debouncedSearch]);
@@ -55,20 +42,18 @@ const useTenderListPresenter = () => {
// }
// };
const allTenders = data?.pages.flatMap((page) => page.data.tenders) ?? [];
return {
ref,
currentTender,
setCurrentTender,
search,
handleFilterChange,
allTenders,
router,
allTenders: data,
error,
isPending,
hasNextPage,
setParams,
data,
fetchNextPage,
isFetchingNextPage,
pathName,
isModalOpen,
setIsModalOpen,