feat(notifications): Implement functional header notification dropdown

This commit replaces the static notification dropdown in the header with a fully functional component that fetches live data from the API.

Key features include:
- Fetches and displays the latest notifications.
- Shows a badge with the count of unseen notifications.
- Opens a modal with notification details upon clicking an item.
- Marks notifications as seen when they are viewed.

Additionally, this commit includes several related improvements:
- refactor: Replaced the global loading component with a custom Tailwind CSS spinner for a consistent look and feel.
- feat: Enhanced the DatePicker component to optionally include a time picker.
- style: Improved the layout of the notification details page by moving the message to a dedicated section and capitalizing the 'priority' and 'type' fields for better readability.
This commit is contained in:
AmirReza Jamali
2025-09-29 15:28:16 +03:30
parent dde2fc5a05
commit 021ee7d11e
11 changed files with 245 additions and 121 deletions
@@ -5,125 +5,109 @@ import {
DropdownContent,
DropdownTrigger,
} from "@/components/ui/dropdown";
import Modal from "@/components/ui/modal";
import { useMarkNotificationAsSeen } from "@/hooks/queries/useNotificationQueries";
import { useIsMobile } from "@/hooks/use-mobile";
import { TNotificationDetailsResponse } from "@/lib/api/types/NotificationHistory"; // Import the type
import { cn } from "@/lib/utils";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { BellIcon } from "./icons";
const notificationList = [
{
image: "/images/user/user-15.png",
title: "Piter Joined the Team!",
subTitle: "Congratulate him",
},
{
image: "/images/user/user-03.png",
title: "New message",
subTitle: "Devid sent a new message",
},
{
image: "/images/user/user-26.png",
title: "New Payment received",
subTitle: "Check your earnings",
},
{
image: "/images/user/user-28.png",
title: "Jolly completed tasks",
subTitle: "Assign new task",
},
{
image: "/images/user/user-27.png",
title: "Roman Joined the Team!",
subTitle: "Congratulate him",
},
];
import NotificationList from "./notification-list";
export function Notification() {
const [isOpen, setIsOpen] = useState(false);
const [isDotVisible, setIsDotVisible] = useState(true);
const [unreadNotificationCount, setUnreadNotificationCount] =
useState<number>(0);
const [isModalOpen, setIsModalOpen] = useState(false);
const [currentNotification, setCurrentNotification] =
useState<TNotificationDetailsResponse | null>(null);
const { mutate } = useMarkNotificationAsSeen(
currentNotification?.id ?? "",
() => setIsModalOpen(false),
);
const isMobile = useIsMobile();
const handleNotificationClick = (
notification: TNotificationDetailsResponse,
) => {
setCurrentNotification(notification);
setIsModalOpen(true);
setIsOpen(false);
};
return (
<Dropdown
isOpen={isOpen}
setIsOpen={(open) => {
setIsOpen(open);
if (setIsDotVisible) setIsDotVisible(false);
}}
>
<DropdownTrigger
className="grid size-12 place-items-center rounded-full border bg-gray-2 text-dark outline-none hover:text-primary focus-visible:border-primary focus-visible:text-primary dark:border-dark-4 dark:bg-dark-3 dark:text-white dark:focus-visible:border-primary"
aria-label="View Notifications"
<>
<Dropdown
isOpen={isOpen}
setIsOpen={(open) => {
setIsOpen(open);
if (setIsDotVisible) setIsDotVisible(false);
}}
>
<span className="relative">
<BellIcon />
{isDotVisible && (
<span
className={cn(
"absolute right-0 top-0 z-1 size-2 rounded-full bg-red-light ring-2 ring-gray-2 dark:ring-dark-3",
)}
>
<span className="absolute inset-0 -z-1 animate-ping rounded-full bg-red-light opacity-75" />
</span>
)}
</span>
</DropdownTrigger>
<DropdownContent
align={isMobile ? "end" : "center"}
className="border border-stroke bg-white px-3.5 py-3 shadow-md dark:border-dark-3 dark:bg-gray-dark min-[350px]:min-w-[20rem]"
>
<div className="mb-1 flex items-center justify-between px-2 py-1.5">
<span className="text-lg font-medium text-dark dark:text-white">
Notifications
</span>
<span className="rounded-md bg-primary px-[9px] py-0.5 text-xs font-medium text-white">
5 new
</span>
</div>
<ul className="mb-3 max-h-[23rem] space-y-1.5 overflow-y-auto">
{notificationList.map((item, index) => (
<li key={index} role="menuitem">
<Link
href="#"
onClick={() => setIsOpen(false)}
className="flex items-center gap-4 rounded-lg px-2 py-1.5 outline-none hover:bg-gray-2 focus-visible:bg-gray-2 dark:hover:bg-dark-3 dark:focus-visible:bg-dark-3"
>
<Image
src={item.image}
className="size-14 rounded-full object-cover"
width={200}
height={200}
alt="User"
/>
<div>
<strong className="block text-sm font-medium text-dark dark:text-white">
{item.title}
</strong>
<span className="truncate text-sm font-medium text-dark-5 dark:text-dark-6">
{item.subTitle}
</span>
</div>
</Link>
</li>
))}
</ul>
<Link
href="#"
onClick={() => setIsOpen(false)}
className="block rounded-lg border border-primary p-2 text-center text-sm font-medium tracking-wide text-primary outline-none transition-colors hover:bg-blue-light-5 focus:bg-blue-light-5 focus:text-primary focus-visible:border-primary dark:border-dark-3 dark:text-dark-6 dark:hover:border-dark-5 dark:hover:bg-dark-3 dark:hover:text-dark-7 dark:focus-visible:border-dark-5 dark:focus-visible:bg-dark-3 dark:focus-visible:text-dark-7"
<DropdownTrigger
className="grid size-12 place-items-center rounded-full border bg-gray-2 text-dark outline-none hover:text-primary focus-visible:border-primary focus-visible:text-primary dark:border-dark-4 dark:bg-dark-3 dark:text-white dark:focus-visible:border-primary"
aria-label="View Notifications"
>
See all notifications
</Link>
</DropdownContent>
</Dropdown>
<span className="relative">
<BellIcon />
{isDotVisible && (
<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",
)}
>
<span className="bg-red-light absolute inset-0 -z-1 animate-ping rounded-full opacity-75" />
</span>
)}
</span>
</DropdownTrigger>
<DropdownContent
align={isMobile ? "end" : "center"}
className="border border-stroke bg-white px-3.5 py-3 shadow-md dark:border-dark-3 dark:bg-gray-dark min-[350px]:min-w-[20rem]"
>
<div className="mb-1 flex items-center justify-between px-2 py-1.5">
<span className="text-lg font-medium text-dark dark:text-white">
Notifications
</span>
<span className="rounded-md bg-primary px-[9px] py-0.5 text-xs font-medium text-white">
{unreadNotificationCount} new
</span>
</div>
<NotificationList
setUnreadNotificationCount={setUnreadNotificationCount}
onNotificationClick={handleNotificationClick}
/>
<Link
href="/notification-history"
onClick={() => setIsOpen(false)}
className="block rounded-lg border border-primary p-2 text-center text-sm font-medium tracking-wide text-primary outline-none transition-colors hover:bg-blue-light-5 focus:bg-blue-light-5 focus:text-primary focus-visible:border-primary dark:border-dark-3 dark:text-dark-6 dark:hover:border-dark-5 dark:hover:bg-dark-3 dark:hover:text-dark-7 dark:focus-visible:border-dark-5 dark:focus-visible:bg-dark-3 dark:focus-visible:text-dark-7"
>
See all notifications
</Link>
</DropdownContent>
</Dropdown>
<Modal
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false);
}}
confirmText="Mark as read"
onConfirm={() => {
mutate();
}}
>
<div className="flex flex-col gap-3 max-w-29">
<h2 className="font-lg font-bold">{currentNotification?.title}</h2>
<p>{currentNotification?.message}</p>
</div>
</Modal>
</>
);
}