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