feat: add socket context and notifications listener for real-time event handling
- Introduced `SocketProvider` to manage WebSocket connection and context across the app. - Added `NotificationsListener` component to handle real-time notifications and display feedback via `toast`. - Enabled event subscriptions for projects, groups, collaborators, and user actions.
This commit is contained in:
parent
d7255444f5
commit
ad6ef4c907
63
frontend/components/notifications.tsx
Normal file
63
frontend/components/notifications.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSocket } from "@/lib/socket-context";
|
||||
import { toast } from "sonner";
|
||||
|
||||
/**
|
||||
* Notification component that listens for real-time notifications
|
||||
* and displays them using toast notifications.
|
||||
*/
|
||||
export function NotificationsListener() {
|
||||
const { onNotification, isConnected } = useSocket();
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isConnected) return;
|
||||
|
||||
// Set up notification listener
|
||||
const unsubscribe = onNotification((data) => {
|
||||
// Display notification based on type
|
||||
switch (data.type) {
|
||||
case "project_invitation":
|
||||
toast.info(data.message, {
|
||||
description: `You've been invited to collaborate on ${data.projectName}`,
|
||||
action: {
|
||||
label: "View Project",
|
||||
onClick: () => window.location.href = `/projects/${data.projectId}`,
|
||||
},
|
||||
});
|
||||
break;
|
||||
case "group_update":
|
||||
toast.info(data.message, {
|
||||
description: data.description,
|
||||
action: data.projectId && {
|
||||
label: "View Groups",
|
||||
onClick: () => window.location.href = `/projects/${data.projectId}/groups`,
|
||||
},
|
||||
});
|
||||
break;
|
||||
case "person_added":
|
||||
toast.success(data.message, {
|
||||
description: data.description,
|
||||
});
|
||||
break;
|
||||
case "person_removed":
|
||||
toast.info(data.message, {
|
||||
description: data.description,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
toast.info(data.message);
|
||||
}
|
||||
});
|
||||
|
||||
setInitialized(true);
|
||||
|
||||
// Clean up on unmount
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [isConnected, onNotification]);
|
||||
|
||||
// This component doesn't render anything visible
|
||||
return null;
|
||||
}
|
192
frontend/lib/socket-context.tsx
Normal file
192
frontend/lib/socket-context.tsx
Normal file
@ -0,0 +1,192 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useEffect, useState, ReactNode } from "react";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { useAuth } from "./auth-context";
|
||||
|
||||
// Define the SocketContext type
|
||||
interface SocketContextType {
|
||||
socket: Socket | null;
|
||||
isConnected: boolean;
|
||||
joinProject: (projectId: string) => void;
|
||||
leaveProject: (projectId: string) => void;
|
||||
// Event listeners
|
||||
onProjectUpdated: (callback: (data: any) => void) => () => void;
|
||||
onCollaboratorAdded: (callback: (data: any) => void) => () => void;
|
||||
onGroupCreated: (callback: (data: any) => void) => () => void;
|
||||
onGroupUpdated: (callback: (data: any) => void) => () => void;
|
||||
onPersonAddedToGroup: (callback: (data: any) => void) => () => void;
|
||||
onPersonRemovedFromGroup: (callback: (data: any) => void) => () => void;
|
||||
onNotification: (callback: (data: any) => void) => () => void;
|
||||
}
|
||||
|
||||
// Create the SocketContext
|
||||
const SocketContext = createContext<SocketContextType | undefined>(undefined);
|
||||
|
||||
// Create a provider component
|
||||
export function SocketProvider({ children }: { children: ReactNode }) {
|
||||
const [socket, setSocket] = useState<Socket | null>(null);
|
||||
const [isConnected, setIsConnected] = useState<boolean>(false);
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
|
||||
// Initialize socket connection when user is authenticated
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated || !user) {
|
||||
return;
|
||||
}
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
||||
|
||||
// Create socket connection
|
||||
const socketInstance = io(API_URL, {
|
||||
withCredentials: true,
|
||||
query: {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Set up event listeners
|
||||
socketInstance.on('connect', () => {
|
||||
console.log('Socket connected');
|
||||
setIsConnected(true);
|
||||
});
|
||||
|
||||
socketInstance.on('disconnect', () => {
|
||||
console.log('Socket disconnected');
|
||||
setIsConnected(false);
|
||||
});
|
||||
|
||||
socketInstance.on('connect_error', (error) => {
|
||||
console.error('Socket connection error:', error);
|
||||
setIsConnected(false);
|
||||
});
|
||||
|
||||
// Save socket instance
|
||||
setSocket(socketInstance);
|
||||
|
||||
// Clean up on unmount
|
||||
return () => {
|
||||
socketInstance.disconnect();
|
||||
setSocket(null);
|
||||
setIsConnected(false);
|
||||
};
|
||||
}, [isAuthenticated, user]);
|
||||
|
||||
// Join a project room
|
||||
const joinProject = (projectId: string) => {
|
||||
if (socket && isConnected) {
|
||||
socket.emit('project:join', projectId);
|
||||
}
|
||||
};
|
||||
|
||||
// Leave a project room
|
||||
const leaveProject = (projectId: string) => {
|
||||
if (socket && isConnected) {
|
||||
socket.emit('project:leave', projectId);
|
||||
}
|
||||
};
|
||||
|
||||
// Event listeners with cleanup
|
||||
const onProjectUpdated = (callback: (data: any) => void) => {
|
||||
if (socket) {
|
||||
socket.on('project:updated', callback);
|
||||
}
|
||||
return () => {
|
||||
if (socket) {
|
||||
socket.off('project:updated', callback);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const onCollaboratorAdded = (callback: (data: any) => void) => {
|
||||
if (socket) {
|
||||
socket.on('project:collaboratorAdded', callback);
|
||||
}
|
||||
return () => {
|
||||
if (socket) {
|
||||
socket.off('project:collaboratorAdded', callback);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const onGroupCreated = (callback: (data: any) => void) => {
|
||||
if (socket) {
|
||||
socket.on('group:created', callback);
|
||||
}
|
||||
return () => {
|
||||
if (socket) {
|
||||
socket.off('group:created', callback);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const onGroupUpdated = (callback: (data: any) => void) => {
|
||||
if (socket) {
|
||||
socket.on('group:updated', callback);
|
||||
}
|
||||
return () => {
|
||||
if (socket) {
|
||||
socket.off('group:updated', callback);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const onPersonAddedToGroup = (callback: (data: any) => void) => {
|
||||
if (socket) {
|
||||
socket.on('group:personAdded', callback);
|
||||
}
|
||||
return () => {
|
||||
if (socket) {
|
||||
socket.off('group:personAdded', callback);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const onPersonRemovedFromGroup = (callback: (data: any) => void) => {
|
||||
if (socket) {
|
||||
socket.on('group:personRemoved', callback);
|
||||
}
|
||||
return () => {
|
||||
if (socket) {
|
||||
socket.off('group:personRemoved', callback);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const onNotification = (callback: (data: any) => void) => {
|
||||
if (socket) {
|
||||
socket.on('notification:new', callback);
|
||||
}
|
||||
return () => {
|
||||
if (socket) {
|
||||
socket.off('notification:new', callback);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Create the context value
|
||||
const value = {
|
||||
socket,
|
||||
isConnected,
|
||||
joinProject,
|
||||
leaveProject,
|
||||
onProjectUpdated,
|
||||
onCollaboratorAdded,
|
||||
onGroupCreated,
|
||||
onGroupUpdated,
|
||||
onPersonAddedToGroup,
|
||||
onPersonRemovedFromGroup,
|
||||
onNotification,
|
||||
};
|
||||
|
||||
return <SocketContext.Provider value={value}>{children}</SocketContext.Provider>;
|
||||
}
|
||||
|
||||
// Create a hook to use the SocketContext
|
||||
export function useSocket() {
|
||||
const context = useContext(SocketContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useSocket must be used within a SocketProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user