59 lines
3.2 KiB
TypeScript
59 lines
3.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useNotifications } from "@lynkd/messaging-inbox-sdk";
|
|
import { useUi } from "@/lib/ui-state";
|
|
import { relativeTime } from "@/lib/format";
|
|
import { AtSign, Heart, LifeBuoy, Bell, MessageSquare, X, CheckCheck } from "lucide-react";
|
|
|
|
const kindIcon: Record<string, typeof Bell> = {
|
|
MENTION: AtSign, REACTION: Heart, SUPPORT_UPDATE: LifeBuoy, THREAD_REPLY: MessageSquare, SYSTEM: Bell,
|
|
};
|
|
|
|
export function NotificationsPanel() {
|
|
const { notifOpen, setNotifOpen } = useUi();
|
|
const { notifications } = useNotifications();
|
|
const router = useRouter();
|
|
const [readIds, setReadIds] = useState<Set<string>>(new Set());
|
|
if (!notifOpen) return null;
|
|
|
|
const isRead = (id: string, base?: boolean) => base || readIds.has(id);
|
|
const markAll = () => setReadIds(new Set(notifications.map((n) => n.id)));
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-40" onClick={() => setNotifOpen(false)}>
|
|
<div className="absolute right-3 top-[58px] w-[min(360px,calc(100vw-1.5rem))] overflow-hidden rounded-card border border-border bg-elevated shadow-pop animate-slide-up" onClick={(e) => e.stopPropagation()}>
|
|
<div className="flex items-center justify-between border-b border-border px-4 py-3">
|
|
<span className="text-[15px] font-bold">Notifications</span>
|
|
<div className="flex items-center gap-1">
|
|
<button onClick={markAll} title="Mark all as read" className="flex items-center gap-1 rounded-control px-2 py-1 text-[11.5px] font-semibold text-muted hover:bg-hover hover:text-ink"><CheckCheck size={14} /> Mark all read</button>
|
|
<button onClick={() => setNotifOpen(false)} className="text-muted hover:text-ink"><X size={16} /></button>
|
|
</div>
|
|
</div>
|
|
<div className="max-h-[60vh] overflow-y-auto">
|
|
{notifications.map((n) => {
|
|
const Icon = kindIcon[n.kind] ?? Bell;
|
|
const read = isRead(n.id, n.read);
|
|
return (
|
|
<button
|
|
key={n.id}
|
|
onClick={() => { setReadIds((s) => new Set(s).add(n.id)); setNotifOpen(false); if (n.channelId) router.push(`/c/${n.channelId}${n.messageId ? `?msg=${n.messageId}` : ""}`); else router.push("/inbox"); }}
|
|
className="flex w-full items-start gap-3 border-b border-border px-4 py-3 text-left hover:bg-hover"
|
|
>
|
|
<span className="mt-0.5 grid h-8 w-8 shrink-0 place-items-center rounded-control bg-accent-soft text-accent"><Icon size={15} /></span>
|
|
<span className="min-w-0 flex-1">
|
|
<span className="text-[13px]"><b className="text-ink">{n.title}</b> <span className="text-muted">{n.body}</span></span>
|
|
<span className="mt-0.5 block text-[11px] text-dim">{relativeTime(n.ts)}</span>
|
|
</span>
|
|
{!read && <span className="mt-1 h-2 w-2 shrink-0 rounded-full bg-accent" />}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
<button onClick={() => { setNotifOpen(false); router.push("/inbox"); }} className="w-full py-2.5 text-center text-[13px] font-semibold text-accent hover:bg-hover">Open Inbox →</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|