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 */}
{/* 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)}
/>
))
)}
);
}