NotificationsPage.tsx
1 import { useState } from 'react' 2 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' 3 import { useAuthStore } from '../store/auth' 4 import type { NotificationType } from '../types/notification' 5 import * as notificationService from '../services/notification' 6 import NotificationCard from '../components/notifications/NotificationCard' 7 8 const NOTIFICATION_FILTERS: { value: NotificationType | 'all'; label: string }[] = [ 9 { value: 'all', label: 'All' }, 10 { value: 'vote_started', label: 'Votes' }, 11 { value: 'pr_sponsored', label: 'PRs' }, 12 { value: 'comment_reply', label: 'Comments' }, 13 { value: 'emergency_action', label: 'Emergency' }, 14 ] 15 16 export default function NotificationsPage() { 17 const { address, isConnected } = useAuthStore() 18 const queryClient = useQueryClient() 19 const [filter, setFilter] = useState<NotificationType | 'all'>('all') 20 const [showUnreadOnly, setShowUnreadOnly] = useState(false) 21 22 // Fetch notifications 23 const { data: notifications = [], isLoading } = useQuery({ 24 queryKey: ['notifications', address, filter, showUnreadOnly], 25 queryFn: () => 26 notificationService.getNotifications(address!, { 27 unreadOnly: showUnreadOnly, 28 types: filter === 'all' ? undefined : [filter], 29 limit: 100, 30 }), 31 enabled: !!address, 32 }) 33 34 // Mark all as read 35 const markAllReadMutation = useMutation({ 36 mutationFn: () => notificationService.markAllAsRead(address!), 37 onSuccess: () => { 38 queryClient.invalidateQueries({ queryKey: ['notifications'] }) 39 queryClient.invalidateQueries({ queryKey: ['notificationSummary'] }) 40 }, 41 }) 42 43 // Group by date 44 const groups = notificationService.groupNotificationsByDate(notifications) 45 const unreadCount = notifications.filter((n) => !n.read).length 46 47 if (!isConnected) { 48 return ( 49 <div className="text-center py-12"> 50 <p className="text-gray-500">Connect your wallet to view notifications</p> 51 </div> 52 ) 53 } 54 55 return ( 56 <div> 57 {/* Header */} 58 <div className="flex items-center justify-between mb-6"> 59 <div> 60 <h1 className="text-2xl font-bold">Notifications</h1> 61 <p className="text-gray-600 text-sm mt-1"> 62 {unreadCount > 0 ? `${unreadCount} unread` : 'All caught up'} 63 </p> 64 </div> 65 66 <div className="flex items-center gap-4"> 67 {unreadCount > 0 && ( 68 <button 69 onClick={() => markAllReadMutation.mutate()} 70 disabled={markAllReadMutation.isPending} 71 className="text-sm text-blue-600 hover:underline" 72 > 73 Mark all as read 74 </button> 75 )} 76 </div> 77 </div> 78 79 {/* Filters */} 80 <div className="flex items-center gap-4 mb-6"> 81 <div className="flex bg-gray-100 rounded-lg p-1"> 82 {NOTIFICATION_FILTERS.map(({ value, label }) => ( 83 <button 84 key={value} 85 onClick={() => setFilter(value)} 86 className={`px-3 py-1.5 rounded-md text-sm ${ 87 filter === value 88 ? 'bg-white shadow text-gray-900 font-medium' 89 : 'text-gray-600 hover:text-gray-900' 90 }`} 91 > 92 {label} 93 </button> 94 ))} 95 </div> 96 97 <label className="flex items-center gap-2 cursor-pointer"> 98 <input 99 type="checkbox" 100 checked={showUnreadOnly} 101 onChange={(e) => setShowUnreadOnly(e.target.checked)} 102 className="w-4 h-4 rounded border-gray-300" 103 /> 104 <span className="text-sm text-gray-600">Unread only</span> 105 </label> 106 </div> 107 108 {/* Notification List */} 109 {isLoading ? ( 110 <div className="flex justify-center py-12"> 111 <div className="animate-spin h-8 w-8 border-2 border-gray-400 border-t-transparent rounded-full" /> 112 </div> 113 ) : notifications.length === 0 ? ( 114 <div className="text-center py-12 bg-white border rounded-lg"> 115 <BellOffIcon className="h-12 w-12 mx-auto text-gray-300" /> 116 <p className="text-gray-500 mt-3">No notifications</p> 117 {filter !== 'all' && ( 118 <button 119 onClick={() => setFilter('all')} 120 className="mt-2 text-sm text-blue-600 hover:underline" 121 > 122 Clear filter 123 </button> 124 )} 125 </div> 126 ) : ( 127 <div className="bg-white border rounded-lg divide-y"> 128 {groups.map((group) => ( 129 <div key={group.date}> 130 <div className="px-4 py-2 bg-gray-50 text-sm font-medium text-gray-600 sticky top-0"> 131 {group.date} 132 </div> 133 <div className="divide-y"> 134 {group.notifications.map((notification) => ( 135 <NotificationCard 136 key={notification.id} 137 notification={notification} 138 /> 139 ))} 140 </div> 141 </div> 142 ))} 143 </div> 144 )} 145 </div> 146 ) 147 } 148 149 function BellOffIcon({ className }: { className?: string }) { 150 return ( 151 <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"> 152 <path 153 strokeLinecap="round" 154 strokeLinejoin="round" 155 strokeWidth={2} 156 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" 157 /> 158 <path 159 strokeLinecap="round" 160 strokeLinejoin="round" 161 strokeWidth={2} 162 d="M3 3l18 18" 163 /> 164 </svg> 165 ) 166 }