Add comprehensive SignalR client implementation with connection management, React hooks, and UI components for real-time notifications and project updates. Changes: - Install @microsoft/signalr package (v9.0.6) - Create SignalR connection manager with auto-reconnect - Implement useNotificationHub hook for notification hub - Implement useProjectHub hook for project hub with room-based subscriptions - Add NotificationPopover UI component with badge and dropdown - Create Badge UI component - Add SignalRProvider for global connection initialization - Update Header to display real-time notifications - Update app layout to include SignalRProvider - Add comprehensive documentation in SIGNALR_INTEGRATION.md Features: - JWT authentication with automatic token management - Auto-reconnect with exponential backoff (0s, 2s, 5s, 10s, 30s) - Connection state management and indicators - Real-time notification push - Project event subscriptions (create, update, delete, status change) - Room-based project subscriptions - Typing indicators support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
99 lines
3.6 KiB
TypeScript
99 lines
3.6 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { Bell } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useNotificationHub } from '@/lib/hooks/useNotificationHub';
|
|
import { Badge } from '@/components/ui/badge';
|
|
|
|
export function NotificationPopover() {
|
|
const { notifications, connectionState, clearNotifications, isConnected } =
|
|
useNotificationHub();
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
const unreadCount = notifications.length;
|
|
|
|
return (
|
|
<div className="relative">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className="relative"
|
|
>
|
|
<Bell className="h-5 w-5" />
|
|
{unreadCount > 0 && (
|
|
<Badge
|
|
className="absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center p-0 text-xs"
|
|
variant="destructive"
|
|
>
|
|
{unreadCount > 9 ? '9+' : unreadCount}
|
|
</Badge>
|
|
)}
|
|
</Button>
|
|
|
|
{isOpen && (
|
|
<div className="absolute right-0 z-50 mt-2 w-80 rounded-lg border border-gray-200 bg-white shadow-lg">
|
|
<div className="flex items-center justify-between border-b border-gray-200 p-4">
|
|
<div className="flex items-center gap-2">
|
|
<h3 className="font-semibold">Notifications</h3>
|
|
<div
|
|
className={`h-2 w-2 rounded-full ${
|
|
isConnected ? 'bg-green-500' : 'bg-gray-400'
|
|
}`}
|
|
/>
|
|
</div>
|
|
{unreadCount > 0 && (
|
|
<Button variant="ghost" size="sm" onClick={clearNotifications}>
|
|
Clear all
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="max-h-96 overflow-y-auto">
|
|
{notifications.length === 0 ? (
|
|
<div className="p-8 text-center text-gray-500">
|
|
<Bell className="mx-auto mb-2 h-12 w-12 opacity-50" />
|
|
<p>No notifications</p>
|
|
</div>
|
|
) : (
|
|
<div className="divide-y">
|
|
{notifications.map((notification, index) => (
|
|
<div key={index} className="p-4 hover:bg-gray-50">
|
|
<div className="flex items-start gap-3">
|
|
<div
|
|
className={`mt-2 h-2 w-2 rounded-full ${
|
|
notification.type === 'error'
|
|
? 'bg-red-500'
|
|
: notification.type === 'success'
|
|
? 'bg-green-500'
|
|
: notification.type === 'warning'
|
|
? 'bg-yellow-500'
|
|
: 'bg-blue-500'
|
|
}`}
|
|
/>
|
|
<div className="flex-1">
|
|
<p className="text-sm">{notification.message}</p>
|
|
<p className="mt-1 text-xs text-gray-500">
|
|
{new Date(notification.timestamp).toLocaleString()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="border-t border-gray-200 p-2 text-center text-xs text-gray-500">
|
|
{connectionState === 'connected' && 'Connected'}
|
|
{connectionState === 'connecting' && 'Connecting...'}
|
|
{connectionState === 'reconnecting' && 'Reconnecting...'}
|
|
{connectionState === 'disconnected' && 'Disconnected'}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|