Files
goodgo-platform/apps/web/lib/notifications-store.ts
Ho Ngoc Hai 0676b8c7f2 feat(notifications): wire client Socket.IO to /notifications namespace with toast + E2E
- Connect to /notifications namespace (matches backend NotificationsGateway)
- Pass JWT token in Socket.IO auth handshake for proper authentication
- Listen for server-pushed notification:unread-count to sync badge
- Show sonner toast on notification:new events
- Add setUnreadCount action to notifications store
- Add E2E round-trip tests (auth connect, reject invalid, multi-device)
- Fix inquiry handler test: event name inquiry.created → inquiry.received

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-21 05:35:44 +07:00

102 lines
2.5 KiB
TypeScript

import { create } from 'zustand';
import { notificationsApi, type NotificationDto } from './notifications-api';
interface NotificationsState {
/** Most recent notifications (for dropdown) */
notifications: NotificationDto[];
/** Unread count for badge */
unreadCount: number;
/** Whether the dropdown is open */
isOpen: boolean;
/** Loading state */
isLoading: boolean;
// Actions
setOpen: (open: boolean) => void;
fetchUnreadCount: () => Promise<void>;
fetchRecent: () => Promise<void>;
markAsRead: (id: string) => Promise<void>;
markAllAsRead: () => Promise<void>;
addNotification: (notification: NotificationDto) => void;
incrementUnread: () => void;
/** Set the unread count directly (from server-pushed WS event). */
setUnreadCount: (count: number) => void;
}
export const useNotificationsStore = create<NotificationsState>((set, get) => ({
notifications: [],
unreadCount: 0,
isOpen: false,
isLoading: false,
setOpen: (open) => {
set({ isOpen: open });
// Fetch latest when opening dropdown
if (open) {
get().fetchRecent();
}
},
fetchUnreadCount: async () => {
try {
const { count } = await notificationsApi.unreadCount();
set({ unreadCount: count });
} catch {
// Silently fail — badge just stays at current count
}
},
fetchRecent: async () => {
set({ isLoading: true });
try {
const result = await notificationsApi.list({ limit: 10 });
set({
notifications: result.data,
isLoading: false,
});
} catch {
set({ isLoading: false });
}
},
markAsRead: async (id) => {
try {
await notificationsApi.markAsRead(id);
set((state) => ({
notifications: state.notifications.map((n) =>
n.id === id ? { ...n, isRead: true } : n,
),
unreadCount: Math.max(0, state.unreadCount - 1),
}));
} catch {
// Silently fail
}
},
markAllAsRead: async () => {
try {
await notificationsApi.markAllAsRead();
set((state) => ({
notifications: state.notifications.map((n) => ({ ...n, isRead: true })),
unreadCount: 0,
}));
} catch {
// Silently fail
}
},
addNotification: (notification) => {
set((state) => ({
notifications: [notification, ...state.notifications].slice(0, 10),
}));
},
incrementUnread: () => {
set((state) => ({ unreadCount: state.unreadCount + 1 }));
},
setUnreadCount: (count) => {
set({ unreadCount: count });
},
}));