NotificationCenter.tsx
1 import { useState } from 'react' 2 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' 3 import { Link } from 'react-router-dom' 4 import { useAuthStore } from '../../store/auth' 5 import type { NotificationGroup } from '../../types/notification' 6 import * as notificationService from '../../services/notification' 7 import NotificationCard from './NotificationCard' 8 9 interface NotificationCenterProps { 10 onClose?: () => void 11 } 12 13 export default function NotificationCenter({ onClose }: NotificationCenterProps) { 14 const { address } = useAuthStore() 15 const queryClient = useQueryClient() 16 const [filter, setFilter] = useState<'all' | 'unread'>('all') 17 18 // Fetch notifications 19 const { data: notifications = [], isLoading } = useQuery({ 20 queryKey: ['notifications', address, filter], 21 queryFn: () => 22 notificationService.getNotifications(address!, { 23 unreadOnly: filter === 'unread', 24 limit: 50, 25 }), 26 enabled: !!address, 27 }) 28 29 // Mark all as read mutation 30 const markAllReadMutation = useMutation({ 31 mutationFn: () => notificationService.markAllAsRead(address!), 32 onSuccess: () => { 33 queryClient.invalidateQueries({ queryKey: ['notifications', address] }) 34 queryClient.invalidateQueries({ queryKey: ['notificationSummary', address] }) 35 }, 36 }) 37 38 // Group notifications by date 39 const groups = notificationService.groupNotificationsByDate(notifications) 40 const unreadCount = notifications.filter((n) => !n.read).length 41 42 return ( 43 <div className="flex flex-col max-h-[80vh]"> 44 {/* Header */} 45 <div className="p-4 border-b flex items-center justify-between"> 46 <h3 className="font-semibold">Notifications</h3> 47 <div className="flex items-center gap-2"> 48 {unreadCount > 0 && ( 49 <button 50 onClick={() => markAllReadMutation.mutate()} 51 disabled={markAllReadMutation.isPending} 52 className="text-xs text-blue-600 hover:underline" 53 > 54 Mark all read 55 </button> 56 )} 57 <Link 58 to="/settings/notifications" 59 onClick={onClose} 60 className="text-gray-400 hover:text-gray-600" 61 > 62 <SettingsIcon className="h-4 w-4" /> 63 </Link> 64 </div> 65 </div> 66 67 {/* Filter tabs */} 68 <div className="px-4 py-2 border-b bg-gray-50"> 69 <div className="flex gap-4"> 70 <button 71 onClick={() => setFilter('all')} 72 className={`text-sm pb-1 border-b-2 ${ 73 filter === 'all' 74 ? 'border-blue-500 text-blue-600 font-medium' 75 : 'border-transparent text-gray-500 hover:text-gray-700' 76 }`} 77 > 78 All 79 </button> 80 <button 81 onClick={() => setFilter('unread')} 82 className={`text-sm pb-1 border-b-2 ${ 83 filter === 'unread' 84 ? 'border-blue-500 text-blue-600 font-medium' 85 : 'border-transparent text-gray-500 hover:text-gray-700' 86 }`} 87 > 88 Unread {unreadCount > 0 && `(${unreadCount})`} 89 </button> 90 </div> 91 </div> 92 93 {/* Notification list */} 94 <div className="flex-1 overflow-y-auto"> 95 {isLoading ? ( 96 <div className="flex justify-center py-8"> 97 <div className="animate-spin h-6 w-6 border-2 border-gray-400 border-t-transparent rounded-full" /> 98 </div> 99 ) : notifications.length === 0 ? ( 100 <div className="py-12 text-center"> 101 <BellOffIcon className="h-12 w-12 mx-auto text-gray-300" /> 102 <p className="text-gray-500 mt-2">No notifications</p> 103 </div> 104 ) : ( 105 <div className="divide-y"> 106 {groups.map((group) => ( 107 <NotificationGroupSection 108 key={group.date} 109 group={group} 110 onClose={onClose} 111 /> 112 ))} 113 </div> 114 )} 115 </div> 116 117 {/* Footer */} 118 {notifications.length > 0 && ( 119 <div className="p-3 border-t bg-gray-50 text-center"> 120 <Link 121 to="/notifications" 122 onClick={onClose} 123 className="text-sm text-blue-600 hover:underline" 124 > 125 View all notifications 126 </Link> 127 </div> 128 )} 129 </div> 130 ) 131 } 132 133 function NotificationGroupSection({ 134 group, 135 onClose, 136 }: { 137 group: NotificationGroup 138 onClose?: () => void 139 }) { 140 return ( 141 <div> 142 <div className="px-4 py-2 bg-gray-50 text-xs font-medium text-gray-500 sticky top-0"> 143 {group.date} 144 </div> 145 <div className="divide-y"> 146 {group.notifications.map((notification) => ( 147 <NotificationCard 148 key={notification.id} 149 notification={notification} 150 onClose={onClose} 151 /> 152 ))} 153 </div> 154 </div> 155 ) 156 } 157 158 function SettingsIcon({ className }: { className?: string }) { 159 return ( 160 <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"> 161 <path 162 strokeLinecap="round" 163 strokeLinejoin="round" 164 strokeWidth={2} 165 d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" 166 /> 167 <path 168 strokeLinecap="round" 169 strokeLinejoin="round" 170 strokeWidth={2} 171 d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" 172 /> 173 </svg> 174 ) 175 } 176 177 function BellOffIcon({ className }: { className?: string }) { 178 return ( 179 <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"> 180 <path 181 strokeLinecap="round" 182 strokeLinejoin="round" 183 strokeWidth={2} 184 d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" 185 /> 186 <path 187 strokeLinecap="round" 188 strokeLinejoin="round" 189 strokeWidth={2} 190 d="M3 3l18 18" 191 /> 192 </svg> 193 ) 194 }