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:
@@ -1,39 +1,188 @@
|
||||
"use client";
|
||||
import Loading from "@/components/loading";
|
||||
|
||||
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
|
||||
import { NotificationDetailsCard } from "@/components/ui/notification-details-card";
|
||||
import { useGetNotificationDetails } from "@/hooks/queries/useNotificationQueries";
|
||||
import { BreadcrumbItem } from "@/types/shared";
|
||||
import { use } from "react";
|
||||
import Loading from "@/components/loading";
|
||||
import Status from "@/components/ui/Status";
|
||||
import { BellIcon } from "@/components/Layouts/header/notification/icons";
|
||||
import Link from "next/link";
|
||||
import { useNotificationDetailsPresenter } from "./useNotificationDetailsPresenter";
|
||||
|
||||
interface IProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
const NotificationDetailsPage = ({ params }: IProps) => {
|
||||
const { id } = use(params);
|
||||
const { data, isLoading } = useGetNotificationDetails(id);
|
||||
const { isLoading, rootRef, breadcrumbItems, meta, details } =
|
||||
useNotificationDetailsPresenter({ params });
|
||||
|
||||
const breadcrumbItems: BreadcrumbItem[] = [
|
||||
{
|
||||
href: "/",
|
||||
name: "Dashboard",
|
||||
},
|
||||
{
|
||||
href: "/notification-history",
|
||||
name: "Notification history",
|
||||
},
|
||||
{
|
||||
href: `/notification-history/${id}`,
|
||||
name: "Notification details",
|
||||
},
|
||||
];
|
||||
if (isLoading) return <Loading />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
{data?.data && <NotificationDetailsCard data={data.data} />}
|
||||
</>
|
||||
<div ref={rootRef} className="flex flex-col gap-6">
|
||||
<div data-anim="breadcrumb">
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
</div>
|
||||
|
||||
{/* Hero header */}
|
||||
<section
|
||||
data-anim="hero"
|
||||
className="relative isolate overflow-hidden rounded-3xl border border-stroke/60 bg-gradient-to-br from-primary via-primary to-blue-700 p-6 text-white shadow-1 md:p-9 dark:border-transparent dark:shadow-card"
|
||||
>
|
||||
<div
|
||||
data-anim="aurora"
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -inset-[40%] z-0 opacity-50"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"conic-gradient(from 0deg at 50% 50%, #5750F1 0deg, #0ABEF9 80deg, transparent 140deg, #8155FF 200deg, #5750F1 280deg, transparent 320deg, #5750F1 360deg)",
|
||||
filter: "blur(64px)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
data-orb="a"
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -right-24 -top-32 z-0 h-72 w-72 rounded-full bg-white/20 blur-3xl"
|
||||
/>
|
||||
<div
|
||||
data-orb="b"
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -bottom-32 -left-16 z-0 h-64 w-64 rounded-full bg-[#0ABEF9]/40 blur-3xl"
|
||||
/>
|
||||
<div
|
||||
data-orb="c"
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute left-1/2 top-1/3 z-0 h-52 w-52 -translate-x-1/2 rounded-full bg-[#8155FF]/30 blur-3xl"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 z-0 rounded-3xl ring-1 ring-inset ring-white/10"
|
||||
/>
|
||||
|
||||
<div className="relative z-10 flex flex-col gap-6 md:flex-row md:items-start">
|
||||
<span
|
||||
data-anim="hero-icon"
|
||||
className="grid size-16 shrink-0 place-items-center rounded-2xl bg-white/20 text-white shadow-theme-sm ring-1 ring-white/30 backdrop-blur-md"
|
||||
>
|
||||
<BellIcon />
|
||||
</span>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<h1
|
||||
className="relative z-[1] text-2xl font-bold leading-tight md:text-3xl"
|
||||
aria-label={meta?.title}
|
||||
>
|
||||
<span
|
||||
data-title-inner
|
||||
className="relative z-[1] inline-block will-change-transform"
|
||||
>
|
||||
{meta?.title}
|
||||
</span>
|
||||
<span
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 z-0 overflow-hidden"
|
||||
>
|
||||
<span
|
||||
data-anim="shine"
|
||||
className="absolute inset-y-0 block w-1/3 opacity-0 blur-[10px] [background:linear-gradient(100deg,transparent_0%,rgba(255,255,255,0.55)_50%,transparent_100%)]"
|
||||
/>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<div className="mt-4 flex flex-wrap items-center gap-2.5">
|
||||
{meta?.priority && (
|
||||
<span data-anim="badge" className="inline-flex">
|
||||
<Status status={meta.priority}>{meta.priority}</Status>
|
||||
</span>
|
||||
)}
|
||||
{meta?.type && (
|
||||
<span
|
||||
data-anim="badge"
|
||||
className="rounded-full bg-white/20 px-3 py-1 text-xs font-semibold capitalize text-white ring-1 ring-white/25 backdrop-blur-sm"
|
||||
>
|
||||
{meta.type}
|
||||
</span>
|
||||
)}
|
||||
{meta?.createdAt && (
|
||||
<span
|
||||
data-anim="badge"
|
||||
className="rounded-full bg-white/15 px-3 py-1 text-xs font-medium text-white/85 ring-1 ring-white/15"
|
||||
>
|
||||
{meta.createdAt}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{meta?.link && (
|
||||
<Link
|
||||
data-anim="badge"
|
||||
href={meta.link}
|
||||
className="mt-4 inline-flex items-center gap-2 rounded-xl bg-white/95 px-4 py-2 text-sm font-semibold text-primary shadow-theme-sm transition-transform hover:scale-[1.03]"
|
||||
>
|
||||
Open link
|
||||
<span aria-hidden>↗</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Detail grid */}
|
||||
<section className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{details.map((item) => (
|
||||
<div
|
||||
key={item.label}
|
||||
data-anim="card"
|
||||
className="group relative overflow-hidden rounded-2xl border border-stroke/60 bg-white p-5 shadow-theme-xs transition-shadow hover:shadow-1 dark:border-dark-3 dark:bg-gray-dark"
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -right-6 -top-6 size-16 rounded-full bg-primary/5 blur-xl transition-opacity group-hover:opacity-100 dark:bg-primary/10"
|
||||
/>
|
||||
<small className="text-xs font-medium uppercase tracking-wide text-dark-5 dark:text-dark-6">
|
||||
{item.label}
|
||||
</small>
|
||||
<div className="mt-2">
|
||||
{item.kind === "status" ? (
|
||||
<Status status={item.tone ?? ""}>{item.value}</Status>
|
||||
) : (
|
||||
<p className="break-words text-base font-semibold text-dark dark:text-white">
|
||||
{item.value}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{/* Image + message */}
|
||||
{(meta?.image || meta?.hasMessage) && (
|
||||
<section
|
||||
data-anim="message"
|
||||
className="overflow-hidden rounded-3xl border border-stroke/60 bg-white shadow-theme-xs dark:border-dark-3 dark:bg-gray-dark"
|
||||
>
|
||||
{meta?.image && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={meta.image}
|
||||
alt={meta.title}
|
||||
className="max-h-72 w-full object-cover"
|
||||
/>
|
||||
)}
|
||||
{meta?.hasMessage && (
|
||||
<div className="p-6 md:p-8">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-lg font-bold text-dark dark:text-white">
|
||||
<span className="inline-block h-5 w-1 rounded-full bg-gradient-to-b from-primary to-blue-700" />
|
||||
Message
|
||||
</h2>
|
||||
<div
|
||||
className="text-base leading-relaxed text-dark-4 dark:text-dark-6 [&_a]:text-primary [&_a]:underline"
|
||||
dangerouslySetInnerHTML={{ __html: meta.messageHtml }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
"use client";
|
||||
|
||||
import { useGetNotificationDetails } from "@/hooks/queries/useNotificationQueries";
|
||||
import { sanitizeHtmlWithStyles } from "@/lib/utils";
|
||||
import { BreadcrumbItem } from "@/types/shared";
|
||||
import { capitalize, unixToDate } from "@/utils/shared";
|
||||
import gsap from "gsap";
|
||||
import { use, useLayoutEffect, useMemo, useRef } from "react";
|
||||
|
||||
interface IParams {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export interface IDetailItem {
|
||||
label: string;
|
||||
value: string;
|
||||
kind?: "text" | "status";
|
||||
tone?: string;
|
||||
}
|
||||
|
||||
const prefersReducedMotion = () =>
|
||||
typeof window !== "undefined" &&
|
||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
export const useNotificationDetailsPresenter = ({ params }: IParams) => {
|
||||
const { id } = use(params);
|
||||
const { data, isLoading } = useGetNotificationDetails(id);
|
||||
const notification = data?.data;
|
||||
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const breadcrumbItems: BreadcrumbItem[] = [
|
||||
{ href: "/", name: "Dashboard" },
|
||||
{ href: "/notification-history", name: "Notification history" },
|
||||
{ href: `/notification-history/${id}`, name: "Notification details" },
|
||||
];
|
||||
|
||||
const meta = useMemo(() => {
|
||||
if (!notification) return null;
|
||||
return {
|
||||
title: notification.title || "Notification",
|
||||
messageHtml: sanitizeHtmlWithStyles(notification.message || ""),
|
||||
hasMessage: !!notification.message,
|
||||
image: notification.image,
|
||||
priority: notification.priority,
|
||||
type: notification.type,
|
||||
status: notification.status,
|
||||
link: notification.link,
|
||||
createdAt: unixToDate({ unix: notification.created_at, hasTime: true }),
|
||||
recipient: notification.recipient?.full_name,
|
||||
};
|
||||
}, [notification]);
|
||||
|
||||
const details: IDetailItem[] = useMemo(() => {
|
||||
if (!notification) return [];
|
||||
return [
|
||||
{
|
||||
label: "Recipient",
|
||||
value: notification.recipient?.full_name || "—",
|
||||
},
|
||||
{ label: "Event type", value: notification.event_type || "—" },
|
||||
{
|
||||
label: "Status",
|
||||
value: capitalize(notification.status || "—"),
|
||||
kind: "status",
|
||||
tone: notification.status,
|
||||
},
|
||||
{
|
||||
label: "Priority",
|
||||
value: capitalize(notification.priority || "—"),
|
||||
kind: "status",
|
||||
tone: notification.priority,
|
||||
},
|
||||
{
|
||||
label: "Channel type",
|
||||
value: capitalize(notification.type || "—"),
|
||||
kind: "status",
|
||||
tone: notification.type,
|
||||
},
|
||||
{
|
||||
label: "Seen",
|
||||
value: notification.seen ? "Yes" : "No",
|
||||
kind: "status",
|
||||
tone: notification.seen ? "success" : "failed",
|
||||
},
|
||||
{
|
||||
label: "Seen at",
|
||||
value: unixToDate({ unix: notification.seen_at ?? 0, hasTime: true }),
|
||||
},
|
||||
{
|
||||
label: "Scheduled",
|
||||
value: notification.is_scheduled ? "Yes" : "No",
|
||||
},
|
||||
];
|
||||
}, [notification]);
|
||||
|
||||
// Complex GSAP intro + ambient motion, scoped to the page root.
|
||||
useLayoutEffect(() => {
|
||||
if (isLoading || !notification) return;
|
||||
|
||||
const root = rootRef.current;
|
||||
if (!root) return;
|
||||
|
||||
const ctx = gsap.context((self) => {
|
||||
if (prefersReducedMotion()) return;
|
||||
|
||||
const q = self.selector!;
|
||||
|
||||
// Entrance timeline
|
||||
const tl = gsap.timeline({ defaults: { ease: "power3.out" } });
|
||||
|
||||
tl.from(root, { y: 28, duration: 0.7 }, 0)
|
||||
.from(
|
||||
q("[data-anim='breadcrumb']"),
|
||||
{ y: -12, opacity: 0, duration: 0.5 },
|
||||
0.05,
|
||||
)
|
||||
.from(
|
||||
q("[data-anim='hero']"),
|
||||
{ y: 40, opacity: 0, duration: 0.8 },
|
||||
0.1,
|
||||
)
|
||||
.from(
|
||||
q("[data-anim='hero-icon']"),
|
||||
{
|
||||
scale: 0,
|
||||
rotate: -120,
|
||||
opacity: 0,
|
||||
duration: 0.8,
|
||||
ease: "back.out(2)",
|
||||
},
|
||||
0.3,
|
||||
)
|
||||
.from(
|
||||
q("[data-title-inner]"),
|
||||
{ y: 32, opacity: 0, duration: 0.7, ease: "back.out(1.6)" },
|
||||
0.4,
|
||||
)
|
||||
.from(
|
||||
q("[data-anim='badge']"),
|
||||
{
|
||||
y: 16,
|
||||
scale: 0.8,
|
||||
opacity: 0,
|
||||
duration: 0.5,
|
||||
stagger: 0.08,
|
||||
ease: "back.out(1.8)",
|
||||
},
|
||||
0.55,
|
||||
)
|
||||
.from(
|
||||
q("[data-anim='card']"),
|
||||
{
|
||||
y: 36,
|
||||
opacity: 0,
|
||||
scale: 0.94,
|
||||
duration: 0.6,
|
||||
stagger: 0.07,
|
||||
ease: "back.out(1.4)",
|
||||
},
|
||||
0.5,
|
||||
)
|
||||
.from(
|
||||
q("[data-anim='message']"),
|
||||
{ y: 30, opacity: 0, duration: 0.7 },
|
||||
0.75,
|
||||
);
|
||||
|
||||
// Ambient aurora rotation
|
||||
const aurora = q("[data-anim='aurora']");
|
||||
if (aurora.length) {
|
||||
gsap.to(aurora, {
|
||||
rotate: 360,
|
||||
duration: 44,
|
||||
ease: "none",
|
||||
repeat: -1,
|
||||
});
|
||||
}
|
||||
|
||||
// Floating orbs
|
||||
gsap.to(q("[data-orb='a']"), {
|
||||
x: 40,
|
||||
y: -28,
|
||||
scale: 1.18,
|
||||
duration: 7,
|
||||
repeat: -1,
|
||||
yoyo: true,
|
||||
ease: "sine.inOut",
|
||||
});
|
||||
gsap.to(q("[data-orb='b']"), {
|
||||
x: -34,
|
||||
y: 26,
|
||||
scale: 1.12,
|
||||
duration: 8.5,
|
||||
repeat: -1,
|
||||
yoyo: true,
|
||||
ease: "sine.inOut",
|
||||
});
|
||||
gsap.to(q("[data-orb='c']"), {
|
||||
x: 24,
|
||||
y: 18,
|
||||
scale: 1.08,
|
||||
duration: 9.5,
|
||||
repeat: -1,
|
||||
yoyo: true,
|
||||
ease: "sine.inOut",
|
||||
});
|
||||
|
||||
// Title shine sweep
|
||||
const shine = q("[data-anim='shine']");
|
||||
if (shine.length) {
|
||||
const shineTl = gsap.timeline({ repeat: -1, repeatDelay: 3.5 });
|
||||
shineTl
|
||||
.set(shine, { xPercent: -160, opacity: 0 })
|
||||
.to(shine, { opacity: 1, duration: 0.2, delay: 1.6 })
|
||||
.to(shine, { xPercent: 260, duration: 1.8, ease: "power2.inOut" }, "<")
|
||||
.to(shine, { opacity: 0, duration: 0.2 });
|
||||
}
|
||||
|
||||
// Card hover lift + magnetic mouse parallax on the hero orbs
|
||||
const cards = q("[data-anim='card']");
|
||||
cards.forEach((card: Element) => {
|
||||
const el = card as HTMLElement;
|
||||
const enter = () =>
|
||||
gsap.to(el, { y: -6, duration: 0.35, ease: "power2.out" });
|
||||
const leave = () =>
|
||||
gsap.to(el, { y: 0, duration: 0.45, ease: "power2.out" });
|
||||
el.addEventListener("mouseenter", enter);
|
||||
el.addEventListener("mouseleave", leave);
|
||||
});
|
||||
|
||||
const onMove = (e: MouseEvent) => {
|
||||
const r = root.getBoundingClientRect();
|
||||
const px = (e.clientX - r.left) / r.width - 0.5;
|
||||
const py = (e.clientY - r.top) / r.height - 0.5;
|
||||
gsap.to(q("[data-orb='a']"), {
|
||||
xPercent: px * 22,
|
||||
yPercent: py * 18,
|
||||
duration: 1.2,
|
||||
ease: "power2.out",
|
||||
overwrite: "auto",
|
||||
});
|
||||
gsap.to(q("[data-orb='b']"), {
|
||||
xPercent: px * -18,
|
||||
yPercent: py * 20,
|
||||
duration: 1.3,
|
||||
ease: "power2.out",
|
||||
overwrite: "auto",
|
||||
});
|
||||
gsap.to(q("[data-orb='c']"), {
|
||||
xPercent: px * 14,
|
||||
yPercent: py * -14,
|
||||
duration: 1.4,
|
||||
ease: "power2.out",
|
||||
overwrite: "auto",
|
||||
});
|
||||
};
|
||||
root.addEventListener("mousemove", onMove);
|
||||
|
||||
return () => root.removeEventListener("mousemove", onMove);
|
||||
}, rootRef);
|
||||
|
||||
return () => ctx.revert();
|
||||
}, [isLoading, notification]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
rootRef,
|
||||
breadcrumbItems,
|
||||
notification,
|
||||
meta,
|
||||
details,
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -70,10 +70,10 @@ export const _TooltipDefaultParams = ({
|
||||
delayHide: 120,
|
||||
variant,
|
||||
className: "fancy-tooltip",
|
||||
border: `1px solid ${palette.border}`,
|
||||
style: {
|
||||
background: `linear-gradient(135deg, ${palette.from} 0%, ${palette.to} 100%)`,
|
||||
boxShadow: palette.shadow,
|
||||
border: `1px solid ${palette.border}`,
|
||||
...styles,
|
||||
} satisfies CSSProperties,
|
||||
};
|
||||
|
||||
@@ -124,6 +124,9 @@ export const useUpdateCustomer = (id: string) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [API_ENDPOINTS.CUSTOMERS.READ_ALL],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["GET ALL CUSTOMERS", id],
|
||||
});
|
||||
router.push("/customers");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -28,18 +28,11 @@ export const useGetMyNotifications = (params?: Record<string, any>) => {
|
||||
queryFn: () => notificationService.getMyNotifications(params),
|
||||
});
|
||||
};
|
||||
export const useMarkNotificationAsSeen = (
|
||||
id: string,
|
||||
successCallback?: () => void,
|
||||
) => {
|
||||
export const useMarkNotificationAsSeen = (successCallback?: () => void) => {
|
||||
const queryClient = useQueryClient();
|
||||
const mutationKey = useMemo(
|
||||
() => [API_ENDPOINTS.NOTIFICATIONS.MARK_AS_SEEN(id), id],
|
||||
[id],
|
||||
);
|
||||
return useMutation({
|
||||
mutationKey,
|
||||
mutationFn: () => notificationService.markNotificationAsSeen(id),
|
||||
mutationKey: ["MARK_NOTIFICATION_AS_SEEN"],
|
||||
mutationFn: (id: string) => notificationService.markNotificationAsSeen(id),
|
||||
onSuccess: () => {
|
||||
if (successCallback) {
|
||||
successCallback();
|
||||
|
||||
Reference in New Issue
Block a user