feat(notifications): Implement notification details page and card

This commit introduces a dedicated page for viewing the full details of a notification and refactors the related UI components for better modularity and user experience.

Key changes include:
- Created a new reusable `NotificationDetailsCard` component to display notification details, decoupling the presentation logic from the page.
- Refactored the notification details page (`/notification-history/[id]`) to use the new card component, resulting in cleaner code.
- Made notification items in the header dropdown clickable, linking each to its respective details page.
- Added a dedicated "Mark as Read" button to each notification item in the dropdown for improved usability.
- Updated the application's global title metadata for branding consistency.
- Added a specific page title for the Tenders page to improve SEO and navigation.
This commit is contained in:
AmirReza Jamali
2025-09-30 12:43:41 +03:30
parent cfb95d3bb1
commit ae89c45a7e
9 changed files with 114 additions and 58 deletions
@@ -103,7 +103,7 @@ export function Notification() {
mutate();
}}
>
<div className="flex flex-col gap-3 max-w-29">
<div className="flex flex-col gap-3 max-w-prose">
<h2 className="font-lg font-bold">{currentNotification?.title}</h2>
<p>{currentNotification?.message}</p>
</div>
@@ -16,7 +16,7 @@ const NotificationList = ({
setUnreadNotificationCount,
onNotificationClick,
}: IProps) => {
const { data, isLoading } = useGetMyNotifications({ seen: false });
const { data, isLoading } = useGetMyNotifications({seen:false});
useEffect(() => {
setUnreadNotificationCount(data?.data.length ?? 0);
}, [data, isLoading]);
@@ -1,5 +1,5 @@
"use client";
import { EyeIcon } from "@/assets/icons";
import { CheckIcon, CrossIcon, EyeIcon } from "@/assets/icons";
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
import Pagination from "@/components/ui/pagination";
import Status from "@/components/ui/Status";
@@ -13,7 +13,7 @@ import {
} from "@/components/ui/table";
import TableSkeleton from "@/components/ui/TableSkeleton";
import { _TooltipDefaultParams } from "@/constants/tooltip";
import { isoStringToDate } from "@/utils/shared";
import { unixToDate } from "@/utils/shared";
import Link from "next/link";
import { Tooltip } from "react-tooltip";
import useNotificationHistoryTablePresenter from "./useNotificationHistoryTablePresenter";
@@ -59,9 +59,9 @@ const NotificationHistoryTable = () => {
<TableCell colSpan={100}>{item.title}</TableCell>
<TableCell colSpan={100}>{item.event_type}</TableCell>
<TableCell colSpan={100}>
{isoStringToDate({
{unixToDate({
hasTime: true,
isoString: item.created_at,
unix: item.created_at,
})}
</TableCell>
<TableCell colSpan={100}>{item.message}</TableCell>
@@ -72,7 +72,7 @@ const NotificationHistoryTable = () => {
{item.type}
</TableCell>
<TableCell className="capitalize" colSpan={100}>
{item.seen ? "Seen" : "Unseen"}
{item.seen ? <CheckIcon fill="green"/> : <CrossIcon width={15} fill="red" />}
</TableCell>
<TableCell colSpan={100}>
<Status status={item.status}>{item.status}</Status>
@@ -0,0 +1,67 @@
"use client";
import { TNotificationDetailsResponse } from "@/lib/api/types/NotificationHistory";
import { capitalize, unixToDate } from "@/utils/shared";
import { FC } from "react";
import IsVisible from "./IsVisible";
import Status from "./Status";
interface IProps {
data: TNotificationDetailsResponse;
}
export const NotificationDetailsCard: FC<IProps> = ({ data }) => {
return (
<div className="rounded-2xl bg-white p-6 shadow-default dark:bg-gray-dark">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<DetailItem title="Recipient" value={data.recipient?.full_name} />
<DetailItem title="Event Type" value={data.event_type} />
<DetailItem title="Link" value={data.link} />
<DetailItem title="Priority">
<Status status={data.priority ?? ""}>
{capitalize(data.priority ?? "")}
</Status>
</DetailItem>
<DetailItem title="Type">
<Status status={data.type ?? ""}>
{capitalize(data.type ?? "")}
</Status>
</DetailItem>
<DetailItem
title="Seen at"
value={unixToDate({ unix: data.seen_at ?? 0, hasTime: false })}
/>
<DetailItem
title="Is Scheduled"
value={data.is_scheduled ? "Yes" : "No"}
/>
<DetailItem title="Has seen">
<Status status={data.seen ? "success" : "failed"}>
{data.seen ? "Yes" : "No"}
</Status>
</DetailItem>
</div>
<IsVisible condition={!!data.message}>
<hr className="my-5" />
<DetailItem title="Message">
<p className="text-base leading-relaxed">{data.message}</p>
</DetailItem>
</IsVisible>
</div>
);
};
const DetailItem: FC<{
title: string;
value?: string;
children?: React.ReactNode;
}> = ({ title, value, children }) => {
return (
<div className="rounded-lg bg-gray-2 p-4 shadow-sm dark:bg-gray-7">
<small className="text-sm font-light text-black dark:text-white">
{title}
</small>
{value ? <p className="mt-2 text-base font-normal">{value}</p> : null}
{children ? <div className="mt-1">{children}</div> : null}
</div>
);
};