Files
goodgo-platform/apps/web/lib/hooks/use-socket-notifications.ts
Ho Ngoc Hai 4400d0c123 feat: add real-time notification system with Socket.IO client
Implements the frontend notification client for TEC-2217:

1. notifications-api.ts — API client for list, unread-count,
   markAsRead, markAllAsRead endpoints
2. notifications-store.ts — Zustand store for notification state
   (recent list, unread count, dropdown open state)
3. use-socket-notifications.ts — Socket.IO hook that connects with
   httpOnly cookie auth, listens for notification:new events,
   auto-reconnects, and syncs unread count on (re)connect
4. notification-bell.tsx — Bell icon with unread badge + dropdown
   showing 10 most recent notifications with time-ago formatting,
   mark-as-read on click, mark-all-as-read, and "Xem tất cả" link
5. notifications-provider.tsx — Provider wired into locale layout
   (inside AuthProvider) to initialize Socket.IO connection
6. Dashboard header — NotificationBell placed before LanguageSwitcher

Added socket.io-client dependency.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-16 02:24:21 +07:00

75 lines
2.2 KiB
TypeScript

'use client';
import { useEffect, useRef } from 'react';
import { io, type Socket } from 'socket.io-client';
import { useAuthStore } from '@/lib/auth-store';
import type { NotificationDto } from '@/lib/notifications-api';
import { useNotificationsStore } from '@/lib/notifications-store';
const SOCKET_URL = process.env['NEXT_PUBLIC_API_URL']?.replace('/api/v1', '') || 'http://localhost:3001';
/**
* Hook that manages the Socket.IO connection for real-time notifications.
*
* - Connects when user is authenticated
* - Listens for `notification:new` events
* - Auto-reconnects on disconnect
* - Disconnects on logout
*/
export function useSocketNotifications() {
const socketRef = useRef<Socket | null>(null);
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const { addNotification, incrementUnread, fetchUnreadCount } =
useNotificationsStore();
useEffect(() => {
if (!isAuthenticated) {
// Disconnect if user logs out
if (socketRef.current) {
socketRef.current.disconnect();
socketRef.current = null;
}
return;
}
// Don't create duplicate connections
if (socketRef.current?.connected) return;
const socket = io(SOCKET_URL, {
path: '/socket.io',
withCredentials: true, // Send httpOnly auth cookies
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1000,
reconnectionDelayMax: 10000,
autoConnect: true,
});
socket.on('connect', () => {
// Fetch unread count on (re)connect to sync state
fetchUnreadCount();
});
socket.on('notification:new', (data: NotificationDto) => {
addNotification(data);
incrementUnread();
});
socket.on('disconnect', (reason) => {
// Socket.IO auto-reconnects for transport errors.
// Only manual disconnects ('io client disconnect') need explicit reconnect.
if (reason === 'io server disconnect') {
socket.connect();
}
});
socketRef.current = socket;
return () => {
socket.disconnect();
socketRef.current = null;
};
}, [isAuthenticated, addNotification, incrementUnread, fetchUnreadCount]);
}