Replace all console.log/warn/error statements with unified logger utility. Changes: - Replaced console in lib/hooks/use-stories.ts - Replaced console in lib/signalr/SignalRContext.tsx - Replaced console in lib/hooks/useProjectHub.ts - Replaced console in lib/hooks/use-tasks.ts - Replaced console in lib/hooks/useNotificationHub.ts - Replaced console in lib/hooks/use-projects.ts - Replaced console in app/(dashboard)/projects/[id]/kanban/page.tsx Logger respects NODE_ENV (debug disabled in production). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
290 lines
8.1 KiB
TypeScript
290 lines
8.1 KiB
TypeScript
'use client';
|
|
|
|
import React, { createContext, useContext, useEffect, useState, useCallback, useRef } from 'react';
|
|
import { HubConnection } from '@microsoft/signalr';
|
|
import { SignalRConnectionManager, ConnectionState } from './ConnectionManager';
|
|
import { SIGNALR_CONFIG } from './config';
|
|
import { useAuthStore } from '@/stores/authStore';
|
|
import { toast } from 'sonner';
|
|
import { logger } from '@/lib/utils/logger';
|
|
|
|
// ============================================
|
|
// TYPE DEFINITIONS
|
|
// ============================================
|
|
|
|
interface SignalRContextValue {
|
|
// Connection management
|
|
connection: HubConnection | null;
|
|
connectionState: ConnectionState;
|
|
isConnected: boolean;
|
|
|
|
// Event handlers registry
|
|
service: SignalREventService | null;
|
|
|
|
// Manual connection control (optional)
|
|
connect: () => Promise<void>;
|
|
disconnect: () => Promise<void>;
|
|
}
|
|
|
|
// Event subscription service
|
|
interface SignalREventService {
|
|
subscribe: (eventName: string, handler: (...args: any[]) => void) => () => void;
|
|
unsubscribe: (eventName: string, handler?: (...args: any[]) => void) => void;
|
|
getEventHandlers: () => SignalREventService | null;
|
|
}
|
|
|
|
// ============================================
|
|
// CONTEXT CREATION
|
|
// ============================================
|
|
|
|
const SignalRContext = createContext<SignalRContextValue | null>(null);
|
|
|
|
// ============================================
|
|
// PROVIDER COMPONENT
|
|
// ============================================
|
|
|
|
interface SignalRProviderProps {
|
|
children: React.ReactNode;
|
|
hubUrl?: string; // Optional: custom hub URL (defaults to PROJECT hub)
|
|
autoConnect?: boolean; // Auto-connect when authenticated (default: true)
|
|
showToasts?: boolean; // Show connection status toasts (default: true)
|
|
}
|
|
|
|
export function SignalRProvider({
|
|
children,
|
|
hubUrl = SIGNALR_CONFIG.HUB_URLS.PROJECT,
|
|
autoConnect = true,
|
|
showToasts = true,
|
|
}: SignalRProviderProps) {
|
|
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
|
const [connectionState, setConnectionState] = useState<ConnectionState>('disconnected');
|
|
const managerRef = useRef<SignalRConnectionManager | null>(null);
|
|
const eventHandlersRef = useRef<Map<string, Set<(...args: any[]) => void>>>(new Map());
|
|
|
|
// ============================================
|
|
// EVENT SERVICE IMPLEMENTATION
|
|
// ============================================
|
|
|
|
const eventService: SignalREventService = {
|
|
subscribe: (eventName: string, handler: (...args: any[]) => void) => {
|
|
// Register handler
|
|
if (!eventHandlersRef.current.has(eventName)) {
|
|
eventHandlersRef.current.set(eventName, new Set());
|
|
}
|
|
eventHandlersRef.current.get(eventName)?.add(handler);
|
|
|
|
// Subscribe to SignalR event
|
|
if (managerRef.current) {
|
|
managerRef.current.on(eventName, handler);
|
|
}
|
|
|
|
// Return unsubscribe function
|
|
return () => {
|
|
eventHandlersRef.current.get(eventName)?.delete(handler);
|
|
if (managerRef.current) {
|
|
managerRef.current.off(eventName, handler);
|
|
}
|
|
};
|
|
},
|
|
|
|
unsubscribe: (eventName: string, handler?: (...args: any[]) => void) => {
|
|
if (handler) {
|
|
eventHandlersRef.current.get(eventName)?.delete(handler);
|
|
if (managerRef.current) {
|
|
managerRef.current.off(eventName, handler);
|
|
}
|
|
} else {
|
|
// Unsubscribe all handlers for this event
|
|
eventHandlersRef.current.delete(eventName);
|
|
if (managerRef.current) {
|
|
managerRef.current.off(eventName);
|
|
}
|
|
}
|
|
},
|
|
|
|
getEventHandlers: () => eventService,
|
|
};
|
|
|
|
// ============================================
|
|
// CONNECTION MANAGEMENT
|
|
// ============================================
|
|
|
|
const connect = useCallback(async () => {
|
|
if (!isAuthenticated) {
|
|
logger.warn('[SignalRContext] Cannot connect: user not authenticated');
|
|
return;
|
|
}
|
|
|
|
if (managerRef.current?.state === 'connected') {
|
|
logger.debug('[SignalRContext] Already connected');
|
|
return;
|
|
}
|
|
|
|
const manager = new SignalRConnectionManager(hubUrl);
|
|
managerRef.current = manager;
|
|
|
|
// Subscribe to state changes
|
|
manager.onStateChange((state) => {
|
|
setConnectionState(state);
|
|
|
|
// Show toast notifications
|
|
if (showToasts) {
|
|
if (state === 'connected') {
|
|
toast.success('Connected to real-time updates');
|
|
} else if (state === 'disconnected') {
|
|
toast.error('Disconnected from real-time updates');
|
|
} else if (state === 'reconnecting') {
|
|
toast.info('Reconnecting...');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Re-subscribe all registered event handlers
|
|
eventHandlersRef.current.forEach((handlers, eventName) => {
|
|
handlers.forEach((handler) => {
|
|
manager.on(eventName, handler);
|
|
});
|
|
});
|
|
|
|
try {
|
|
await manager.start();
|
|
} catch (error) {
|
|
logger.error('[SignalRContext] Connection error:', error);
|
|
if (showToasts) {
|
|
toast.error('Failed to connect to real-time updates');
|
|
}
|
|
}
|
|
}, [isAuthenticated, hubUrl, showToasts]);
|
|
|
|
const disconnect = useCallback(async () => {
|
|
if (managerRef.current) {
|
|
await managerRef.current.stop();
|
|
managerRef.current = null;
|
|
}
|
|
}, []);
|
|
|
|
// ============================================
|
|
// AUTO-CONNECT ON AUTHENTICATION
|
|
// ============================================
|
|
|
|
useEffect(() => {
|
|
if (autoConnect && isAuthenticated) {
|
|
connect();
|
|
} else if (!isAuthenticated) {
|
|
disconnect();
|
|
}
|
|
|
|
return () => {
|
|
disconnect();
|
|
};
|
|
}, [isAuthenticated, autoConnect, connect, disconnect]);
|
|
|
|
// ============================================
|
|
// CONTEXT VALUE
|
|
// ============================================
|
|
|
|
const contextValue: SignalRContextValue = {
|
|
connection: managerRef.current?.['connection'] ?? null,
|
|
connectionState,
|
|
isConnected: connectionState === 'connected',
|
|
service: eventService,
|
|
connect,
|
|
disconnect,
|
|
};
|
|
|
|
return (
|
|
<SignalRContext.Provider value={contextValue}>
|
|
{children}
|
|
</SignalRContext.Provider>
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// CUSTOM HOOKS
|
|
// ============================================
|
|
|
|
/**
|
|
* Access SignalR context
|
|
*/
|
|
export function useSignalRContext(): SignalRContextValue {
|
|
const context = useContext(SignalRContext);
|
|
if (!context) {
|
|
throw new Error('useSignalRContext must be used within SignalRProvider');
|
|
}
|
|
return context;
|
|
}
|
|
|
|
/**
|
|
* Subscribe to a specific SignalR event (simplified hook)
|
|
*
|
|
* @example
|
|
* useSignalREvent('TaskStatusChanged', (taskId, newStatus) => {
|
|
* console.log('Task status changed:', taskId, newStatus);
|
|
* });
|
|
*/
|
|
export function useSignalREvent(
|
|
eventName: string,
|
|
handler: (...args: any[]) => void,
|
|
deps: React.DependencyList = []
|
|
) {
|
|
const { service, isConnected } = useSignalRContext();
|
|
|
|
useEffect(() => {
|
|
if (!isConnected || !service) {
|
|
return;
|
|
}
|
|
|
|
const unsubscribe = service.subscribe(eventName, handler);
|
|
|
|
return () => {
|
|
unsubscribe();
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [eventName, isConnected, service, ...deps]);
|
|
}
|
|
|
|
/**
|
|
* Subscribe to multiple SignalR events
|
|
*
|
|
* @example
|
|
* useSignalREvents({
|
|
* 'TaskCreated': (task) => console.log('Task created:', task),
|
|
* 'TaskUpdated': (task) => console.log('Task updated:', task),
|
|
* });
|
|
*/
|
|
export function useSignalREvents(
|
|
events: Record<string, (...args: any[]) => void>,
|
|
deps: React.DependencyList = []
|
|
) {
|
|
const { service, isConnected } = useSignalRContext();
|
|
|
|
useEffect(() => {
|
|
if (!isConnected || !service) {
|
|
return;
|
|
}
|
|
|
|
const unsubscribers = Object.entries(events).map(([eventName, handler]) =>
|
|
service.subscribe(eventName, handler)
|
|
);
|
|
|
|
return () => {
|
|
unsubscribers.forEach((unsubscribe) => unsubscribe());
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [isConnected, service, ...deps]);
|
|
}
|
|
|
|
/**
|
|
* Get connection state and status
|
|
*/
|
|
export function useSignalRConnection() {
|
|
const { connectionState, isConnected, connect, disconnect } = useSignalRContext();
|
|
|
|
return {
|
|
connectionState,
|
|
isConnected,
|
|
connect,
|
|
disconnect,
|
|
};
|
|
}
|