"use client"; import { useEffect, useRef } from "react"; import { Bell, Check, Trash2, ListMusic, AlertCircle, CheckCircle, ExternalLink, } from "lucide-react"; import { api } from "@/lib/api"; import { cn } from "@/utils/cn"; import Link from "next/link"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; interface Notification { id: string; type: string; title: string; message: string | null; metadata: any; read: boolean; createdAt: string; } export function NotificationsTab() { const queryClient = useQueryClient(); const previousNotificationIds = useRef>(new Set()); const { data: notifications = [], isLoading: loading, error, } = useQuery({ queryKey: ["notifications"], queryFn: async () => { console.log("[NotificationsTab] Fetching notifications..."); const result = await api.getNotifications(); console.log("[NotificationsTab] Got notifications:", result); return result; }, refetchInterval: 30000, // Poll every 30 seconds }); // Dispatch events when new playlist-related notifications arrive useEffect(() => { if (!notifications || notifications.length === 0) return; const currentIds = new Set(notifications.map((n) => n.id)); // Check for new playlist-related notifications for (const notification of notifications) { if (!previousNotificationIds.current.has(notification.id)) { // This is a new notification if ( notification.type === "playlist_ready" || notification.type === "import_complete" ) { console.log( "[NotificationsTab] New playlist notification, dispatching event" ); window.dispatchEvent(new CustomEvent("playlist-created")); } } } previousNotificationIds.current = currentIds; }, [notifications]); // Log error if any if (error) { console.error( "[NotificationsTab] Error fetching notifications:", error ); } // Mark as read - optimistic update const markAsReadMutation = useMutation({ mutationFn: (id: string) => api.markNotificationAsRead(id), onMutate: async (id: string) => { await queryClient.cancelQueries({ queryKey: ["notifications"] }); const previousNotifications = queryClient.getQueryData< Notification[] >(["notifications"]); // Optimistically update queryClient.setQueryData( ["notifications"], (old) => old?.map((n) => (n.id === id ? { ...n, read: true } : n)) || [] ); return { previousNotifications }; }, onError: (_err, _id, context) => { // Rollback on error if (context?.previousNotifications) { queryClient.setQueryData( ["notifications"], context.previousNotifications ); } }, }); // Clear single notification - optimistic update const clearMutation = useMutation({ mutationFn: (id: string) => api.clearNotification(id), onMutate: async (id: string) => { await queryClient.cancelQueries({ queryKey: ["notifications"] }); const previousNotifications = queryClient.getQueryData< Notification[] >(["notifications"]); // Optimistically remove queryClient.setQueryData( ["notifications"], (old) => old?.filter((n) => n.id !== id) || [] ); return { previousNotifications }; }, onError: (_err, _id, context) => { if (context?.previousNotifications) { queryClient.setQueryData( ["notifications"], context.previousNotifications ); } }, }); // Clear all notifications - optimistic update const clearAllMutation = useMutation({ mutationFn: () => api.clearAllNotifications(), onMutate: async () => { await queryClient.cancelQueries({ queryKey: ["notifications"] }); const previousNotifications = queryClient.getQueryData< Notification[] >(["notifications"]); // Optimistically clear all queryClient.setQueryData(["notifications"], []); return { previousNotifications }; }, onError: (_err, _vars, context) => { if (context?.previousNotifications) { queryClient.setQueryData( ["notifications"], context.previousNotifications ); } }, }); const handleMarkAsRead = (id: string) => markAsReadMutation.mutate(id); const handleClear = (id: string) => clearMutation.mutate(id); const handleClearAll = () => clearAllMutation.mutate(); const getIcon = (type: string) => { switch (type) { case "download_complete": return ; case "download_failed": return ; case "playlist_ready": case "import_complete": return ; case "system": default: return ; } }; const getLink = (notification: Notification): string | null => { if (notification.metadata?.playlistId) { return `/playlist/${notification.metadata.playlistId}`; } if (notification.metadata?.albumId) { return `/album/${notification.metadata.albumId}`; } if (notification.metadata?.artistId) { return `/artist/${notification.metadata.artistId}`; } return null; }; const formatTime = (dateStr: string) => { const date = new Date(dateStr); const now = new Date(); const diff = now.getTime() - date.getTime(); if (diff < 60000) return "Just now"; if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`; if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`; return date.toLocaleDateString(); }; if (loading) { return (
); } if (notifications.length === 0) { return (

No notifications

You're all caught up!

); } return (
{/* Header with clear all */} {notifications.length > 0 && (
{notifications.length} notification {notifications.length !== 1 ? "s" : ""}
)} {/* Notification list */}
{notifications.map((notification) => { const link = getLink(notification); return (
{getIcon(notification.type)}

{notification.title}

{!notification.read && ( )}
{notification.message && (

{notification.message}

)}
{formatTime(notification.createdAt)} {link && ( View{" "} )}
{!notification.read && ( )}
); })}
); }