Files
maternal-app/maternal-web/hooks/useWebSocket.ts
Andrei 7f9226b943
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
feat: Complete Real-Time Sync implementation 🔄
BACKEND:
- Fix JWT authentication in FamiliesGateway
  * Configure JwtModule with ConfigService in FamiliesModule
  * Load JWT_SECRET from environment variables
  * Enable proper token verification for WebSocket connections
- Fix circular dependency in TrackingModule
  * Use forwardRef pattern for FamiliesGateway injection
  * Make FamiliesGateway optional in TrackingService
  * Emit WebSocket events when activities are created/updated/deleted

FRONTEND:
- Create WebSocket service (336 lines)
  * Socket.IO client with auto-reconnection (exponential backoff 1s → 30s)
  * Family room join/leave management
  * Presence tracking (online users per family)
  * Event handlers for activities, children, members
  * Connection recovery with auto-rejoin
- Create useWebSocket hook (187 lines)
  * Auto-connect on user authentication
  * Auto-join user's family room
  * Connection status tracking
  * Presence indicators
  * Hooks: useRealTimeActivities, useRealTimeChildren, useRealTimeFamilyMembers
- Expose access token in AuthContext
  * Add token property to AuthContextType interface
  * Load token from tokenStorage on initialization
  * Update token state on login/register/logout
  * Enable WebSocket authentication
- Integrate real-time sync across app
  * AppShell: Connection status indicator + online count badge
  * Activities page: Auto-refresh on family activity events
  * Home page: Auto-refresh daily summary on activity changes
  * Family page: Real-time member updates
- Fix accessibility issues
  * Remove deprecated legacyBehavior from Link components (Next.js 15)
  * Fix color contrast in EmailVerificationBanner (WCAG AA)
  * Add missing aria-labels to IconButtons
  * Fix React key warnings in family member list

DOCUMENTATION:
- Update implementation-gaps.md
  * Mark Real-Time Sync as COMPLETED 
  * Document WebSocket room management implementation
  * Document connection recovery and presence indicators
  * Update summary statistics (49 features completed)

FILES CREATED:
- maternal-web/hooks/useWebSocket.ts (187 lines)
- maternal-web/lib/websocket.ts (336 lines)

FILES MODIFIED (14):
Backend (4):
- families.gateway.ts (JWT verification fix)
- families.module.ts (JWT config with ConfigService)
- tracking.module.ts (forwardRef for FamiliesModule)
- tracking.service.ts (emit WebSocket events)

Frontend (9):
- lib/auth/AuthContext.tsx (expose access token)
- components/layouts/AppShell/AppShell.tsx (connection status + presence)
- app/activities/page.tsx (real-time activity updates)
- app/page.tsx (real-time daily summary refresh)
- app/family/page.tsx (accessibility fixes)
- app/(auth)/login/page.tsx (remove legacyBehavior)
- components/common/EmailVerificationBanner.tsx (color contrast fix)

Documentation (1):
- docs/implementation-gaps.md (updated status)

IMPACT:
 Real-time family collaboration achieved
 Activities sync instantly across all family members' devices
 Presence tracking shows who's online
 Connection recovery handles poor network conditions
 Accessibility improvements (WCAG AA compliance)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-02 22:06:24 +00:00

187 lines
5.7 KiB
TypeScript

import { useEffect, useState, useCallback, useRef } from 'react';
import { websocketService, PresenceUpdate, WebSocketEventCallback } from '@/lib/websocket';
import { useAuth } from '@/lib/auth/AuthContext';
/**
* React Hook for WebSocket Connection
*
* Features:
* - Automatic connection management
* - Family room subscription
* - Connection status
* - Presence indicators
*/
export function useWebSocket() {
const { user, token } = useAuth();
const [isConnected, setIsConnected] = useState(false);
const [presence, setPresence] = useState<PresenceUpdate>({ onlineUsers: [], count: 0 });
const hasInitialized = useRef(false);
console.log('[useWebSocket] Hook called - User:', !!user, 'Token:', !!token, 'Initialized:', hasInitialized.current);
// Connect to WebSocket when user is authenticated
useEffect(() => {
console.log('[useWebSocket] useEffect triggered - User:', !!user, 'Token:', !!token, 'Initialized:', hasInitialized.current);
if (user && token && !hasInitialized.current) {
hasInitialized.current = true;
const backendUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3020';
console.log('[useWebSocket] Connecting to:', backendUrl);
console.log('[useWebSocket] User authenticated:', !!user);
console.log('[useWebSocket] Token available:', !!token);
websocketService.connect({
url: backendUrl,
token,
});
// Subscribe to connection status
const unsubscribe = websocketService.onConnectionStatusChange(setIsConnected);
return () => {
console.log('[useWebSocket] Disconnecting...');
unsubscribe();
websocketService.disconnect();
hasInitialized.current = false;
};
}
}, [user, token]);
// Auto-join family when user is in a family
useEffect(() => {
const familyId = user?.families?.[0]?.familyId;
if (isConnected && familyId) {
console.log('[useWebSocket] Auto-joining family:', familyId);
websocketService.joinFamily(familyId);
return () => {
websocketService.leaveFamily();
};
}
}, [isConnected, user?.families]);
// Subscribe to presence updates
useEffect(() => {
const unsubscribe = websocketService.on<PresenceUpdate>('presenceUpdate', setPresence);
// Get initial presence when family is joined
const unsubscribeJoined = websocketService.on('familyJoined', (data: any) => {
if (data.onlineUsers) {
setPresence({ onlineUsers: data.onlineUsers, count: data.onlineUsers.length });
}
});
return () => {
unsubscribe();
unsubscribeJoined();
};
}, []);
// Join a specific family room
const joinFamily = useCallback((familyId: string) => {
if (isConnected) {
websocketService.joinFamily(familyId);
}
}, [isConnected]);
// Leave current family room
const leaveFamily = useCallback(() => {
websocketService.leaveFamily();
}, []);
return {
isConnected,
presence,
joinFamily,
leaveFamily,
};
}
/**
* Hook for subscribing to specific WebSocket events
*/
export function useWebSocketEvent<T = any>(
event: string,
callback: WebSocketEventCallback<T>,
dependencies: any[] = []
) {
useEffect(() => {
const unsubscribe = websocketService.on<T>(event, callback);
return unsubscribe;
}, [event, ...dependencies]); // eslint-disable-line react-hooks/exhaustive-deps
}
/**
* Hook for real-time activity updates
*
* Automatically updates local state when activities are created/updated/deleted by other family members
*/
export function useRealTimeActivities(
onActivityCreated?: (activity: any) => void,
onActivityUpdated?: (activity: any) => void,
onActivityDeleted?: (data: { activityId: string }) => void
) {
useWebSocketEvent('activityCreated', (activity) => {
console.log('[useRealTimeActivities] Activity created:', activity);
onActivityCreated?.(activity);
}, [onActivityCreated]);
useWebSocketEvent('activityUpdated', (activity) => {
console.log('[useRealTimeActivities] Activity updated:', activity);
onActivityUpdated?.(activity);
}, [onActivityUpdated]);
useWebSocketEvent('activityDeleted', (data) => {
console.log('[useRealTimeActivities] Activity deleted:', data);
onActivityDeleted?.(data);
}, [onActivityDeleted]);
}
/**
* Hook for real-time child updates
*/
export function useRealTimeChildren(
onChildAdded?: (child: any) => void,
onChildUpdated?: (child: any) => void,
onChildDeleted?: (data: { childId: string }) => void
) {
useWebSocketEvent('childAdded', (child) => {
console.log('[useRealTimeChildren] Child added:', child);
onChildAdded?.(child);
}, [onChildAdded]);
useWebSocketEvent('childUpdated', (child) => {
console.log('[useRealTimeChildren] Child updated:', child);
onChildUpdated?.(child);
}, [onChildUpdated]);
useWebSocketEvent('childDeleted', (data) => {
console.log('[useRealTimeChildren] Child deleted:', data);
onChildDeleted?.(data);
}, [onChildDeleted]);
}
/**
* Hook for real-time family member updates
*/
export function useRealTimeFamilyMembers(
onMemberAdded?: (member: any) => void,
onMemberUpdated?: (member: any) => void,
onMemberRemoved?: (data: { userId: string }) => void
) {
useWebSocketEvent('memberAdded', (member) => {
console.log('[useRealTimeFamilyMembers] Member added:', member);
onMemberAdded?.(member);
}, [onMemberAdded]);
useWebSocketEvent('memberUpdated', (member) => {
console.log('[useRealTimeFamilyMembers] Member updated:', member);
onMemberUpdated?.(member);
}, [onMemberUpdated]);
useWebSocketEvent('memberRemoved', (data) => {
console.log('[useRealTimeFamilyMembers] Member removed:', data);
onMemberRemoved?.(data);
}, [onMemberRemoved]);
}