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:
@@ -1,13 +1,10 @@
|
||||
"use client";
|
||||
import Loading from "@/app/loading";
|
||||
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
|
||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||
import IsVisible from "@/components/ui/IsVisible";
|
||||
import { NotificationDetailsCard } from "@/components/ui/notification-details-card";
|
||||
import { useGetNotificationDetails } from "@/hooks/queries/useNotificationQueries";
|
||||
import { BreadcrumbItem } from "@/types/shared";
|
||||
import { unixToDate } from "@/utils/shared";
|
||||
import { use } from "react";
|
||||
import { capitalize } from "../../../../utils/shared";
|
||||
|
||||
interface IProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -35,48 +32,7 @@ const NotificationDetailsPage = ({ params }: IProps) => {
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
<ShowcaseSection title={`${data?.data.title}`}>
|
||||
<div className="mt-4 grid grid-cols-4 gap-15 pb-4">
|
||||
<IsVisible condition={!!data?.data.event_type}>
|
||||
<ShowcaseSection title="Event Type">
|
||||
{data?.data.event_type}
|
||||
</ShowcaseSection>
|
||||
</IsVisible>
|
||||
<IsVisible condition={!!data?.data.link}>
|
||||
<ShowcaseSection title="Link">{data?.data.link}</ShowcaseSection>
|
||||
</IsVisible>
|
||||
|
||||
<IsVisible condition={!!data?.data.priority}>
|
||||
<ShowcaseSection title="Priority">
|
||||
{capitalize(data?.data.priority ?? "")}
|
||||
</ShowcaseSection>
|
||||
</IsVisible>
|
||||
|
||||
<IsVisible condition={!!data?.data.type}>
|
||||
<ShowcaseSection title="Type">
|
||||
{capitalize(data?.data.type ?? "")}
|
||||
</ShowcaseSection>
|
||||
</IsVisible>
|
||||
<IsVisible condition={!!data?.data.seen_at}>
|
||||
<ShowcaseSection title="Seen at">
|
||||
{unixToDate({ unix: data?.data.seen_at ?? 0, hasTime: false })}
|
||||
</ShowcaseSection>
|
||||
</IsVisible>
|
||||
|
||||
<ShowcaseSection title="Is Scheduled">
|
||||
{data?.data.is_scheduled ? "Yes" : "No"}
|
||||
</ShowcaseSection>
|
||||
<ShowcaseSection title="Has seen">
|
||||
{data?.data.seen ? "Yes" : "No"}
|
||||
</ShowcaseSection>
|
||||
</div>
|
||||
<hr className="py-4" />
|
||||
<IsVisible condition={!!data?.data.message}>
|
||||
<ShowcaseSection title="Message">
|
||||
{data?.data.message}
|
||||
</ShowcaseSection>
|
||||
</IsVisible>
|
||||
</ShowcaseSection>
|
||||
{data?.data && <NotificationDetailsCard data={data.data} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import { Providers } from "./providers";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
template: "%s | NextAdmin - Next.js Dashboard Kit",
|
||||
template: "%s | Opp lens",
|
||||
default: "Opp lens - panel",
|
||||
},
|
||||
description:
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
|
||||
import TendersTable from "@/components/Tables/tenders";
|
||||
|
||||
import { Metadata } from "next";
|
||||
export const metadata:Metadata = {
|
||||
title: "Tenders Page",
|
||||
}
|
||||
const TendersPage = () => {
|
||||
const breadcrumbItems = [
|
||||
{ name: "Dashboard", href: "/" },
|
||||
|
||||
+14
-2
@@ -228,6 +228,20 @@ export function CalendarIcon(props: IconProps) {
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
// export function CheckIcon(props: IconProps) {
|
||||
// return (
|
||||
// <svg
|
||||
// xmlns="http://www.w3.org/2000/svg"
|
||||
// height="24px"
|
||||
// viewBox="0 -960 960 960"
|
||||
// width="24px"
|
||||
// fill="currentColor"
|
||||
// {...props}
|
||||
// >
|
||||
// <path d="M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z" />
|
||||
// </svg>
|
||||
// );
|
||||
// }
|
||||
export function EyeIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
@@ -415,9 +429,7 @@ export function CrossIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24"
|
||||
viewBox="0 -960 960 960"
|
||||
width="24"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import { z } from "zod";
|
||||
import { createApiResponseSchema } from "./Factory";
|
||||
|
||||
export const notificationSchema = z.object({
|
||||
created_at: z.string(),
|
||||
created_at: z.number(),
|
||||
event_type: z.string(),
|
||||
id: z.string(),
|
||||
image: z.string(),
|
||||
@@ -35,6 +35,24 @@ export const notificationSchema = z.object({
|
||||
type: z.string(),
|
||||
updated_at: z.string(),
|
||||
user_id: z.string(),
|
||||
recipient: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
full_name: z.string(),
|
||||
username: z.string(),
|
||||
email: z.string(),
|
||||
role: z.string(),
|
||||
status: z.string(),
|
||||
department: z.string(),
|
||||
position: z.string(),
|
||||
phone: z.string(),
|
||||
profile_image: z.string(),
|
||||
is_verified: z.boolean(),
|
||||
last_login_at: z.number(),
|
||||
updated_at: z.number(),
|
||||
created_at: z.number(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
const createNotificationCredentialsSchema = z.object({
|
||||
channels: z.array(z.enum(NotificationChannels)),
|
||||
|
||||
Reference in New Issue
Block a user