feature: version one notification feature implemented.

This commit is contained in:
2026-07-18 01:09:54 +03:30
parent 0740493384
commit 9f6ec193d2
40 changed files with 1679 additions and 224 deletions

View File

@@ -1,4 +1,5 @@
import { apiClient } from '@/lib/api/client';
import type { InboxPage, UserNotificationItem } from '@/types/notifications';
import type { LabCaseActivityItem } from '@/types/lab-case-activity';
import type { LabCaseTabReadTarget, TabBadgeCounts } from '@/lib/tabBadgeUtils';
@@ -27,4 +28,29 @@ export const notificationsApi = {
const response = await apiClient.post('/notifications/mark-case-read', { labCaseId });
return response.data;
},
listInbox: async (params?: {
limit?: number;
cursor?: string;
}): Promise<{ success: boolean; data: InboxPage }> => {
const response = await apiClient.get('/notifications/inbox', { params });
return response.data;
},
inboxUnreadCount: async (): Promise<{ success: boolean; data: { count: number } }> => {
const response = await apiClient.get('/notifications/inbox/unread-count');
return response.data;
},
markInboxRead: async (
id: string,
): Promise<{ success: boolean; data: UserNotificationItem | null }> => {
const response = await apiClient.post(`/notifications/inbox/${id}/read`);
return response.data;
},
markInboxReadAll: async (): Promise<{ success: boolean }> => {
const response = await apiClient.post('/notifications/inbox/read-all');
return response.data;
},
};

View File

@@ -0,0 +1,100 @@
'use client';
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
type ReactNode,
} from 'react';
import { io, type Socket } from 'socket.io-client';
import { useAuth } from '@/lib/hooks/useAuth';
import type { UserNotificationItem } from '@/types/notifications';
type RealtimeContextValue = {
connected: boolean;
lastNotification: UserNotificationItem | null;
unreadCount: number | null;
setUnreadCount: (count: number) => void;
};
const RealtimeContext = createContext<RealtimeContextValue>({
connected: false,
lastNotification: null,
unreadCount: null,
setUnreadCount: () => undefined,
});
function apiOrigin(): string {
const base = process.env.NEXT_PUBLIC_API_URL ?? '';
try {
return new URL(base).origin;
} catch {
return typeof window !== 'undefined' ? window.location.origin : '';
}
}
export function RealtimeProvider({ children }: { children: ReactNode }) {
const { user, currentOrganization, isAuthReady } = useAuth();
const [connected, setConnected] = useState(false);
const [lastNotification, setLastNotification] = useState<UserNotificationItem | null>(null);
const [unreadCount, setUnreadCount] = useState<number | null>(null);
const socketRef = useRef<Socket | null>(null);
useEffect(() => {
if (!isAuthReady || !user || !currentOrganization?.id) {
socketRef.current?.disconnect();
socketRef.current = null;
setConnected(false);
return;
}
const socket = io(`${apiOrigin()}/realtime`, {
withCredentials: true,
transports: ['websocket', 'polling'],
});
socketRef.current = socket;
socket.on('connect', () => setConnected(true));
socket.on('disconnect', () => setConnected(false));
socket.on('notification.created', (payload: { notification?: UserNotificationItem }) => {
if (payload?.notification) {
setLastNotification(payload.notification);
}
});
socket.on('notification.unreadCount', (payload: { count?: number }) => {
if (typeof payload?.count === 'number') {
setUnreadCount(payload.count);
}
});
return () => {
socket.disconnect();
socketRef.current = null;
setConnected(false);
};
}, [isAuthReady, user, currentOrganization?.id]);
const setUnreadCountStable = useCallback((count: number) => {
setUnreadCount(count);
}, []);
const value = useMemo(
() => ({
connected,
lastNotification,
unreadCount,
setUnreadCount: setUnreadCountStable,
}),
[connected, lastNotification, unreadCount, setUnreadCountStable],
);
return <RealtimeContext.Provider value={value}>{children}</RealtimeContext.Provider>;
}
export function useRealtime() {
return useContext(RealtimeContext);
}