From 37c4053a6c845e4ca2360eb609713786884f5e01 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Sat, 2 May 2026 10:05:48 +0330 Subject: [PATCH] feat(tender-details): enhance tender details page with improved layout and error handling - Added SectionHeading and ExternalLinkIcon components for better structure and visual appeal. - Improved error handling UI when tender data fails to load, providing user-friendly messages and navigation options. - Refactored ShowcaseSection to accept additional className and rootClassName props for more flexible styling. - Updated TendersTable to streamline action buttons and improve layout consistency. --- src/app/tenders/[details]/page.tsx | 283 +++++++++++++----- src/components/Layouts/showcase-section.tsx | 14 +- src/components/Tables/tenders/index.tsx | 71 ++--- .../Tables/tenders/useTenderListPresenter.ts | 1 - 4 files changed, 252 insertions(+), 117 deletions(-) diff --git a/src/app/tenders/[details]/page.tsx b/src/app/tenders/[details]/page.tsx index 20febc8..91171b2 100644 --- a/src/app/tenders/[details]/page.tsx +++ b/src/app/tenders/[details]/page.tsx @@ -6,7 +6,8 @@ import Loading from "@/components/loading"; import Status from "@/components/ui/Status"; import { useTenderDetailQuery } from "@/hooks/queries"; import { useGetFlagQuery } from "@/hooks/queries/useFlagsQueries"; -import { isValidAlpha2CountryCode, msToDate } from "@/utils/shared"; +import { cn } from "@/lib/utils"; +import { isValidAlpha2CountryCode, msToDate, truncateString } from "@/utils/shared"; import getSymbolFromCurrency from "currency-symbol-map"; import Link from "next/link"; import { use } from "react"; @@ -17,6 +18,43 @@ interface IProps { }>; } +function SectionHeading({ children }: { children: React.ReactNode }) { + return ( +
+ +

+ {children} +

+ +
+ ); +} + +function ExternalLinkIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +/** Grid row stretch: center value vertically under the title */ +const TIMELINE_SHOWCASE = { + rootClassName: "flex h-full min-h-0 flex-col", + className: "flex min-h-0 flex-1 flex-col justify-center", +} as const; + const TenderDetails = ({ params }: IProps) => { const { details } = use(params); const { data, isLoading, isError } = useTenderDetailQuery(details); @@ -31,86 +69,187 @@ const TenderDetails = ({ params }: IProps) => { { name: "Tenders", href: "/tenders" }, { name: "Tender details", href: `/tenders/${details}` }, ]; + if (isLoading) return ; - if (isError) return
Error
; + if (isError) { + return ( + <> + + +
+

+ We couldn't load this tender +

+

+ Check your connection or try again. You can go back to the tender list anytime. +

+ + Back to tenders + +
+
+ + ); + } + + const tender = data!.data; + const submissionUrl = tender.submission_url?.trim(); + return ( <> - -
-

{data?.data.title}

- - {data?.data.status} - -
-
- - {data?.data.tender_id} - - - {data?.data.buyer_organization.name} - - - {data?.data.notice_publication_id} - - - {data?.data.procedure_code} - - - {data?.data.procurement_type_code} - -
-
- - {msToDate(data?.data.application_deadline ?? 0)} - - {data?.data.submission_url && ( - - - {data?.data.submission_url} - - - )} - - - {msToDate(data?.data.publication_date ?? 0)} - - - {msToDate(data?.data.submission_deadline ?? 0)} - - - {msToDate(data?.data.tender_deadline ?? 0)} - -
-
-
-

Country

-
- -

{data?.data.country_code}

- {flagData && ( -
+ +
+

+ Tender +

+

+ {tender.title} +

+
+
+ {tender.status} + {submissionUrl && ( + + Open submission + + )}
- {data?.data.currency && ( -
-

Currency

-
-

{data?.data.currency}

-

{getSymbolFromCurrency(data?.data.currency ?? "USD")}

-
+ } + className="!p-0" + > +
+
+ Procurement +
+ + {tender.tender_id} + + + + {tender.buyer_organization.name} + + + + + {tender.notice_publication_id} + + + + {tender.procedure_code} + + + + {tender.procurement_type_code} + +
- )} +
-
-
-
-

Description

-

{data?.data.description}

+
+ Timeline +
+ + + + + + + + + + + + + {submissionUrl && ( + + + + {truncateString(submissionUrl, 42)} + + + )} +
+
+ +
+ Location & currency +
+
+
+

+ Country +

+
+ + + +
+ + {tender.country_code} + + {flagData && ( +
+ )} +
+
+
+ + {tender.currency && ( +
+
+

+ Currency +

+
+ + {tender.currency} + + + {getSymbolFromCurrency(tender.currency ?? "USD")} + +
+
+ )} +
+
+ +
+ Description +
+

+ {tender.description || "—"} +

+
+
diff --git a/src/components/Layouts/showcase-section.tsx b/src/components/Layouts/showcase-section.tsx index afc9e3c..994a016 100644 --- a/src/components/Layouts/showcase-section.tsx +++ b/src/components/Layouts/showcase-section.tsx @@ -4,16 +4,24 @@ import type { ReactNode } from "react"; type PropsType = { title?: string | ReactNode; children: ReactNode; + /** Merges into the padded content wrapper below the title */ className?: string; + /** Merges into the outer card (e.g. `h-full flex flex-col` for grid rows) */ + rootClassName?: string; }; -export function ShowcaseSection({ title, children, className }: PropsType) { +export function ShowcaseSection({ title, children, className, rootClassName }: PropsType) { return ( -
+
{title && ( -

+

{title}

)} diff --git a/src/components/Tables/tenders/index.tsx b/src/components/Tables/tenders/index.tsx index a751708..23bf297 100644 --- a/src/components/Tables/tenders/index.tsx +++ b/src/components/Tables/tenders/index.tsx @@ -24,7 +24,6 @@ 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 = () => { const { @@ -89,49 +88,39 @@ const TendersTable = () => { {item.country_code} - - {item.submission_url ? ( - - {item.submission_url} - - ) : ( - "-" - )} - {item.status} - - - + +
+ + +
))} diff --git a/src/components/Tables/tenders/useTenderListPresenter.ts b/src/components/Tables/tenders/useTenderListPresenter.ts index 29a7b03..b12f67b 100644 --- a/src/components/Tables/tenders/useTenderListPresenter.ts +++ b/src/components/Tables/tenders/useTenderListPresenter.ts @@ -57,7 +57,6 @@ const useTenderListPresenter = () => { "row", "title", "country code", - "submission url", "status", "actions", ];