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>
187 lines
5.7 KiB
TypeScript
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]);
|
|
}
|