import { useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { Archive, AtSign, Bell, BellOff, Check, CheckCheck, Loader2, Mail, MessageSquare, Shield, } from "lucide-react"; import { notificationList, notificationMarkRead, notificationMarkAllRead, notificationArchive } from "@/client"; import type { NotificationResponse } from "@/client"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import {getApiErrorMessage} from '@/lib/api-error'; type Filter = "all" | "unread" | "archived"; const NOTIFICATION_TYPE_CONFIG: Record< string, { label: string; icon: React.ReactNode; color: string } > = { mention: { label: "Mention", icon: , color: "bg-blue-500/10 text-blue-600 border-blue-500/20" }, invitation: { label: "Invitation", icon: , color: "bg-purple-500/10 text-purple-600 border-purple-500/20" }, role_change: { label: "Role Change", icon: , color: "bg-orange-500/10 text-orange-600 border-orange-500/20" }, room_created: { label: "Room Created", icon: , color: "bg-green-500/10 text-green-600 border-green-500/20" }, room_deleted: { label: "Room Deleted", icon: , color: "bg-red-500/10 text-red-600 border-red-500/20" }, system_announcement: { label: "Announcement", icon: , color: "bg-yellow-500/10 text-yellow-700 border-yellow-500/20" }, }; function formatTime(dateStr: string): string { const d = new Date(dateStr); const now = new Date(); const diff = now.getTime() - d.getTime(); const minutes = Math.floor(diff / 60000); if (minutes < 1) return "just now"; if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); if (days < 7) return `${days}d ago`; return d.toLocaleDateString(); } function NotificationItem({ n, onMarkRead, onArchive, }: { n: NotificationResponse; onMarkRead: (id: string) => void; onArchive: (id: string) => void; }) { const config = NOTIFICATION_TYPE_CONFIG[n.notification_type] ?? { label: n.notification_type, icon: , color: "bg-muted text-muted-foreground border-border", }; const handleClick = () => { if (!n.is_read) onMarkRead(n.id); }; return (
{/* Unread dot */}
{!n.is_read &&
}
{/* Icon */}
{config.icon}
{/* Content */}
{/* Actions */}
{!n.is_read && ( )}
); } export default function NotifyPage() { const queryClient = useQueryClient(); const [filter, setFilter] = useState("all"); const { data, isLoading } = useQuery({ queryKey: ["notifications", filter], queryFn: async () => { const resp = await notificationList({ query: { only_unread: filter === "unread", archived: filter === "archived" ? true : undefined, limit: 100, }, }); return resp.data?.data ?? null; }, }); const markReadMutation = useMutation({ mutationFn: async (notificationId: string) => { await notificationMarkRead({ path: { notification_id: notificationId } }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["notifications"] }); queryClient.invalidateQueries({ queryKey: ["me"] }); }, onError: (err: unknown) => { toast.error(getApiErrorMessage(err, "Failed to mark as read")); }, }); const markAllReadMutation = useMutation({ mutationFn: async () => { await notificationMarkAllRead(); }, onSuccess: () => { toast.success("All notifications marked as read"); queryClient.invalidateQueries({ queryKey: ["notifications"] }); queryClient.invalidateQueries({ queryKey: ["me"] }); }, onError: (err: unknown) => { toast.error(getApiErrorMessage(err, "Failed to mark all as read")); }, }); const archiveMutation = useMutation({ mutationFn: async (notificationId: string) => { await notificationArchive({ path: { notification_id: notificationId } }); }, onSuccess: () => { toast.success("Notification archived"); queryClient.invalidateQueries({ queryKey: ["notifications"] }); }, onError: (err: unknown) => { toast.error(getApiErrorMessage(err, "Failed to archive")); }, }); const notifications: NotificationResponse[] = data?.notifications ?? []; const total: number = data?.total ?? 0; const unreadCount: number = data?.unread_count ?? 0; return (
{/* Header */}

Notifications

{unreadCount > 0 ? `${unreadCount} unread ยท ${total} total` : `${total} notification${total !== 1 ? "s" : ""}`}

{unreadCount > 0 && filter !== "archived" && ( )}
{/* Filter tabs */}
{(["all", "unread", "archived"] as Filter[]).map((f) => ( ))}
{/* List */}
{isLoading ? (
) : notifications.length === 0 ? (

{filter === "unread" ? "No unread notifications" : filter === "archived" ? "No archived notifications" : "No notifications yet"}

{filter === "unread" ? "You're all caught up!" : filter === "archived" ? "Archived notifications will appear here." : "You'll see notifications here when something happens."}

) : ( notifications.map((n) => ( markReadMutation.mutate(id)} onArchive={(id) => archiveMutation.mutate(id)} /> )) )}
); }