feat(notification): enhance NotificationDetailsPage and Notification component with improved UI and functionality

- Refactored NotificationDetailsPage to utilize a presenter hook for better state management and loading handling.
- Improved UI elements in NotificationDetailsPage, including a hero header and detail grid for displaying notification information.
- Updated Notification component to streamline the marking of notifications as seen and enhance the display of notification details.
- Added gradient backgrounds and improved tooltip functionality for better visual feedback and user experience.
This commit is contained in:
AmirReza Jamali
2026-06-03 16:34:43 +03:30
parent bb3695ee74
commit a99f86b2f9
7 changed files with 553 additions and 63 deletions
@@ -6,10 +6,12 @@ import {
DropdownTrigger,
} from "@/components/ui/dropdown";
import Modal from "@/components/ui/modal";
import Status from "@/components/ui/Status";
import { useMarkNotificationAsSeen } from "@/hooks/queries/useNotificationQueries";
import { useIsMobile } from "@/hooks/use-mobile";
import { TNotificationDetailsResponse } from "@/lib/api/types/NotificationHistory";
import { cn } from "@/lib/utils";
import { cn, sanitizeHtmlWithStyles } from "@/lib/utils";
import { capitalize, unixToDate } from "@/utils/shared";
import Link from "next/link";
import { useState } from "react";
import { BellIcon } from "./icons";
@@ -24,10 +26,7 @@ export function Notification() {
const [currentNotification, setCurrentNotification] =
useState<TNotificationDetailsResponse | null>(null);
const { mutate } = useMarkNotificationAsSeen(
currentNotification?.id ?? "",
() => setIsModalOpen(false),
);
const { mutate } = useMarkNotificationAsSeen();
const isMobile = useIsMobile();
const handleNotificationClick = (
@@ -36,7 +35,7 @@ export function Notification() {
setCurrentNotification(notification);
setIsModalOpen(true);
setIsOpen(false);
mutate();
if (notification.id) mutate(notification.id);
};
return (
@@ -49,18 +48,20 @@ export function Notification() {
}}
>
<DropdownTrigger
className="grid size-12 place-items-center rounded-full border border-stroke/70 bg-white/75 text-dark shadow-theme-xs backdrop-blur-md outline-none transition-colors hover:text-primary focus-visible:border-primary focus-visible:text-primary dark:border-dark-3 dark:bg-dark-2/75 dark:text-white dark:focus-visible:border-primary"
className="shadow-theme-xs grid size-12 place-items-center rounded-full border border-stroke/70 bg-white/75 text-dark outline-none backdrop-blur-md transition-colors hover:text-primary focus-visible:border-primary focus-visible:text-primary dark:border-dark-3 dark:bg-dark-2/75 dark:text-white dark:focus-visible:border-primary"
aria-label="View Notifications"
>
<span className="relative">
<BellIcon />
{isDotVisible && (
{isDotVisible && unreadNotificationCount > 0 && (
<span
className={cn(
"bg-red-light absolute right-0 top-0 z-1 size-2 rounded-full ring-2 ring-gray-2 dark:ring-dark-3",
"absolute -right-1.5 -top-1.5 z-1 grid min-w-[18px] place-items-center rounded-full bg-gradient-to-br from-rose-500 to-red-600 px-1 text-[10px] font-bold leading-none text-white shadow-[0_2px_8px_rgba(239,68,68,0.55)] ring-2 ring-white dark:ring-dark-2",
unreadNotificationCount > 9 ? "h-[18px]" : "size-[18px]",
)}
>
<span className="bg-red-light absolute inset-0 -z-1 animate-ping rounded-full opacity-75" />
<span className="absolute inset-0 -z-1 animate-ping rounded-full bg-rose-500/70" />
{unreadNotificationCount > 99 ? "99+" : unreadNotificationCount}
</span>
)}
</span>
@@ -68,13 +69,13 @@ export function Notification() {
<DropdownContent
align={isMobile ? "end" : "center"}
className="w-[15rem] max-w-[calc(100vw-1rem)] min-[360px]:w-[16rem] min-[390px]:w-[17rem] min-[430px]:w-[18rem] rounded-2xl border border-stroke/70 bg-white/95 px-2.5 py-3 shadow-theme-xs backdrop-blur-md sm:w-[22rem] sm:px-3.5 dark:border-dark-3 dark:bg-gray-dark/95"
className="shadow-theme-xs w-[15rem] max-w-[calc(100vw-1rem)] rounded-2xl border border-stroke/70 bg-white/95 px-2.5 py-3 backdrop-blur-md dark:border-dark-3 dark:bg-gray-dark/95 min-[360px]:w-[16rem] min-[390px]:w-[17rem] min-[430px]:w-[18rem] sm:w-[22rem] sm:px-3.5"
>
<div className="mb-1 flex flex-wrap items-center justify-between gap-2 px-1 py-1.5 sm:px-2">
<span className="text-base font-semibold text-dark sm:text-lg dark:text-white">
<span className="text-base font-semibold text-dark dark:text-white sm:text-lg">
Notifications
</span>
<span className="shrink-0 rounded-full border border-white/35 bg-gradient-to-br from-primary/85 to-primary/70 px-[9px] py-0.5 text-xs font-semibold text-white shadow-theme-xs dark:border-white/20">
<span className="shadow-theme-xs shrink-0 rounded-full border border-white/35 bg-gradient-to-br from-primary/85 to-primary/70 px-[9px] py-0.5 text-xs font-semibold text-white dark:border-white/20">
{unreadNotificationCount} new
</span>
</div>
@@ -101,10 +102,86 @@ export function Notification() {
setIsModalOpen(false);
}}
confirmText="Mark as read"
classNames="relative !p-0 w-[34rem] max-w-[calc(100vw-2rem)] !rounded-3xl border border-stroke/60 dark:border-dark-3 [&>button]:absolute [&>button]:right-3 [&>button]:top-3 [&>button]:z-10 [&>button]:!mb-0 [&>button]:!text-white/80 [&>button]:hover:!text-white"
>
<div className="flex max-w-prose flex-col gap-3">
<h2 className="font-lg font-bold">{currentNotification?.title}</h2>
<p>{currentNotification?.message}</p>
<div className="flex max-w-full flex-col">
{/* Gradient header */}
<div className="relative overflow-hidden bg-gradient-to-br from-primary via-primary to-blue-700 px-6 pb-6 pt-7">
<div className="pointer-events-none absolute -right-8 -top-10 size-32 rounded-full bg-white/10 blur-md" />
<div className="pointer-events-none absolute -bottom-12 -left-6 size-28 rounded-full bg-white/10 blur-md" />
<div className="relative flex items-start gap-4">
<span className="grid size-12 shrink-0 place-items-center rounded-2xl bg-white/20 text-white shadow-theme-sm ring-1 ring-white/30 backdrop-blur-sm">
<BellIcon />
</span>
<div className="min-w-0 flex-1">
<div className="mb-1.5 flex flex-wrap items-center gap-2">
{currentNotification?.priority && (
<Status status={currentNotification.priority}>
{capitalize(currentNotification.priority)}
</Status>
)}
{currentNotification?.type && (
<span className="rounded-full bg-white/20 px-2.5 py-0.5 text-xs font-medium text-white ring-1 ring-white/25">
{capitalize(currentNotification.type)}
</span>
)}
</div>
<h2 className="text-lg font-bold leading-snug text-white">
{currentNotification?.title || "Notification"}
</h2>
{!!currentNotification?.created_at && (
<p className="mt-1 text-xs font-medium text-white/75">
{unixToDate({
unix: currentNotification.created_at,
hasTime: true,
})}
</p>
)}
</div>
</div>
</div>
{/* Body */}
<div className="flex flex-col gap-4 px-6 py-5">
{currentNotification?.image && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={currentNotification.image}
alt={currentNotification.title || "Notification image"}
className="max-h-52 w-full rounded-2xl object-cover ring-1 ring-stroke/60 dark:ring-dark-3"
/>
)}
<div
className="text-sm leading-relaxed text-dark-4 dark:text-dark-6 [&_a]:text-primary [&_a]:underline"
dangerouslySetInnerHTML={{
__html: sanitizeHtmlWithStyles(
currentNotification?.message || "No message content.",
),
}}
/>
<div className="mt-1 flex items-center justify-end gap-3 border-t border-stroke/60 pt-4 dark:border-dark-3">
{currentNotification?.link && (
<Link
href={currentNotification.link}
onClick={() => setIsModalOpen(false)}
className="rounded-xl border border-primary/40 bg-primary/5 px-4 py-2 text-sm font-semibold text-primary transition-colors hover:bg-primary/10"
>
Open link
</Link>
)}
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="rounded-xl bg-gradient-to-br from-primary to-blue-700 px-5 py-2 text-sm font-semibold text-white shadow-theme-sm transition-opacity hover:opacity-90"
>
Got it
</button>
</div>
</div>
</div>
</Modal>
</>
+5 -11
View File
@@ -206,7 +206,6 @@ const CustomersTable = () => {
setIsModalOpen(true);
}}
>
<Tooltip {..._TooltipDefaultParams({ id: "delete" })} />
<span className="sr-only">Delete Company </span>
<TrashIcon />
</button>
@@ -219,7 +218,6 @@ const CustomersTable = () => {
router.push(`${pathName}/edit/${customer.id}`)
}
>
<Tooltip {..._TooltipDefaultParams({ id: "edit" })} />
<span className="sr-only">Edit Company </span>
<PencilSquareIcon />
</button>
@@ -237,9 +235,6 @@ const CustomersTable = () => {
});
}}
>
<Tooltip
{..._TooltipDefaultParams({ id: "assign-company" })}
/>
<span className="sr-only">Assign To Company </span>
<Building />
</button>
@@ -253,11 +248,6 @@ const CustomersTable = () => {
openResetPasswordModalForCustomer(customer)
}
>
<Tooltip
{..._TooltipDefaultParams({
id: "reset-password",
})}
/>
<span className="sr-only">
Reset Customer Password
</span>
@@ -272,7 +262,6 @@ const CustomersTable = () => {
router.push(`${pathName}/feedback/${customer.id}`);
}}
>
<Tooltip {..._TooltipDefaultParams({ id: "feedback" })} />
<ExclamationIcon />
</button>
</div>
@@ -346,6 +335,11 @@ const CustomersTable = () => {
onCancel={() => setIsModalOpen(false)}
/>
)}
<Tooltip {..._TooltipDefaultParams({ id: "delete" })} />
<Tooltip {..._TooltipDefaultParams({ id: "edit" })} />
<Tooltip {..._TooltipDefaultParams({ id: "assign-company" })} />
<Tooltip {..._TooltipDefaultParams({ id: "reset-password" })} />
<Tooltip {..._TooltipDefaultParams({ id: "feedback" })} />
</ListWrapper>
);
};