- 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>
102 lines
2.5 KiB
TypeScript
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 });
|
|
},
|
|
}));
|