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>
This commit is contained in:
Ho Ngoc Hai
2026-04-16 02:24:21 +07:00
parent 3a5d2ca9c1
commit 4400d0c123
9 changed files with 851 additions and 13 deletions

View File

@@ -0,0 +1,52 @@
import { apiClient } from './api-client';
// ─── Types ──────────────────────────────────────────────
export interface NotificationDto {
id: string;
type: string;
title: string;
body: string;
link: string | null;
isRead: boolean;
createdAt: string;
}
export interface PaginatedNotifications {
data: NotificationDto[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface UnreadCountDto {
count: number;
}
// ─── API Functions ──────────────────────────────────────
export const notificationsApi = {
/** Get paginated notifications for the current user */
list: (params: { page?: number; limit?: number } = {}) => {
const query = new URLSearchParams();
if (params.page) query.append('page', String(params.page));
if (params.limit) query.append('limit', String(params.limit));
const qs = query.toString();
return apiClient.get<PaginatedNotifications>(
`/notifications${qs ? `?${qs}` : ''}`,
);
},
/** Get unread notification count */
unreadCount: () =>
apiClient.get<UnreadCountDto>('/notifications/unread-count'),
/** Mark a single notification as read */
markAsRead: (id: string) =>
apiClient.patch<{ success: boolean }>(`/notifications/${id}/read`),
/** Mark all notifications as read */
markAllAsRead: () =>
apiClient.patch<{ success: boolean }>('/notifications/read-all'),
};