diff --git a/backend/controllers/referral.controller.js b/backend/controllers/referral.controller.js
index bc9687d..3e972f4 100644
--- a/backend/controllers/referral.controller.js
+++ b/backend/controllers/referral.controller.js
@@ -69,7 +69,7 @@ async function moderateReferral({ referralId, adminId, actionType, reason, refer
adminId,
referralId: referral._id,
actionType,
- reason: normalizeModerationReason(reason),
+ reason: reason,
targetUserId: referral.postedBy?._id || referral.postedBy || null
});
});
@@ -104,6 +104,30 @@ async function getReferralAccess(referralId, userId) {
};
}
+async function checkReferralAccessMiddleware(req, res, next) {
+ try {
+ const { id } = req.params;
+ if (!id || !mongoose.Types.ObjectId.isValid(id)) {
+ return res.status(400).json({ message: 'Invalid referral ID format' });
+ }
+
+ const accessInfo = await getReferralAccess(id, req.user.id);
+ if (!accessInfo.referral) {
+ return res.status(404).json({ message: 'Referral not found' });
+ }
+
+ // Hide referrals that are hidden/removed from non-admins
+ if (isReferralHidden(accessInfo.referral) && accessInfo.currentUser?.type !== 'admin') {
+ return res.status(404).json({ message: 'Referral not found' });
+ }
+
+ req.referralAccess = accessInfo;
+ next();
+ } catch (err) {
+ next(err);
+ }
+}
+
// Create a new job referral opportunity
async function createReferral(req, res, next) {
try {
@@ -223,22 +247,12 @@ async function getReferrals(req, res, next) {
// Apply for a referral
async function applyForReferral(req, res, next) {
try {
- const { id } = req.params;
- const currentUser = await getCurrentUserSummary(req.user.id);
+ const { referral, currentUser } = req.referralAccess;
if (!currentUser || currentUser.type !== 'student') {
return res.status(403).json({ message: 'Only students can apply for referrals' });
}
- const referral = await JobReferral.findById(id);
- if (!referral) {
- return res.status(404).json({ message: 'Referral not found' });
- }
-
- if (isReferralHidden(referral) && currentUser?.type !== 'admin') {
- return res.status(404).json({ message: 'Referral not found' });
- }
-
if (referral.status !== 'open') {
return res.status(400).json({ message: 'Referral is no longer open' });
}
@@ -287,16 +301,9 @@ async function applyForReferral(req, res, next) {
// Accept a referral applicant
async function acceptReferral(req, res, next) {
try {
- const { id, applicantId } = req.params;
- const currentUser = await getCurrentUserSummary(req.user.id);
+ const { applicantId } = req.params;
+ const { referral, isOwner, isAdmin, currentUser } = req.referralAccess;
- const referral = await JobReferral.findById(id);
- if (!referral) {
- return res.status(404).json({ message: 'Referral not found' });
- }
-
- const isOwner = referral.postedBy.toString() === req.user.id;
- const isAdmin = currentUser?.type === 'admin';
if (!isOwner && !isAdmin) {
return res.status(403).json({ message: 'Access denied' });
}
@@ -346,16 +353,9 @@ async function acceptReferral(req, res, next) {
// Reject a referral applicant
async function rejectReferral(req, res, next) {
try {
- const { id, applicantId } = req.params;
- const currentUser = await getCurrentUserSummary(req.user.id);
+ const { applicantId } = req.params;
+ const { referral, isOwner, isAdmin, currentUser } = req.referralAccess;
- const referral = await JobReferral.findById(id);
- if (!referral) {
- return res.status(404).json({ message: 'Referral not found' });
- }
-
- const isOwner = referral.postedBy.toString() === req.user.id;
- const isAdmin = currentUser?.type === 'admin';
if (!isOwner && !isAdmin) {
return res.status(403).json({ message: 'Access denied' });
}
@@ -399,15 +399,10 @@ async function rejectReferral(req, res, next) {
// Get single referral by ID
async function getReferralById(req, res, next) {
try {
- const { referral, access } = await getReferralAccess(req.params.id, req.user.id);
-
- if (!referral) {
- return res.status(404).json({ message: 'Referral not found' });
- }
+ const { referral, access } = req.referralAccess;
if (!access) {
- // Do not leak referral existence.
- return res.status(404).json({ message: 'Referral not found' });
+ return res.status(403).json({ message: 'Access denied' });
}
// Keep response structure consistent with prior implementation.
@@ -422,15 +417,10 @@ async function getReferralById(req, res, next) {
async function getReferralTimeline(req, res, next) {
try {
- const { referral, access } = await getReferralAccess(req.params.id, req.user.id);
-
- if (!referral) {
- return res.status(404).json({ message: 'Referral not found' });
- }
+ const { referral, access } = req.referralAccess;
if (!access) {
- // Do not leak referral existence.
- return res.status(404).json({ message: 'Referral not found' });
+ return res.status(403).json({ message: 'Access denied' });
}
const timeline = [...(referral.timeline || [])].sort(
@@ -445,18 +435,9 @@ async function getReferralTimeline(req, res, next) {
async function closeReferral(req, res, next) {
-
try {
- const { id } = req.params;
- const currentUser = await getCurrentUserSummary(req.user.id);
-
- const referral = await JobReferral.findById(id);
- if (!referral) {
- return res.status(404).json({ message: 'Referral not found' });
- }
+ const { referral, isOwner, isAdmin, currentUser } = req.referralAccess;
- const isOwner = referral.postedBy.toString() === req.user.id;
- const isAdmin = currentUser?.type === 'admin';
if (!isOwner && !isAdmin) {
return res.status(403).json({ message: 'Access denied' });
}
@@ -545,22 +526,32 @@ async function flagReferralForSpam(req, res, next) {
try {
const { id } = req.params;
const { reason } = req.body;
+ const normalizedReason = normalizeModerationReason(reason, 'Flagged for review');
const { referral, moderationAction } = await moderateReferral({
referralId: id,
adminId: req.user.id,
actionType: 'flag',
- reason: normalizeModerationReason(reason, 'Flagged for review'),
+ reason: normalizedReason,
referralMutator: async (referralDoc) => {
referralDoc.moderation = {
status: 'flagged',
- reason: normalizeModerationReason(reason, 'Flagged for review'),
+ reason: normalizedReason,
moderatedBy: req.user.id,
moderatedAt: new Date()
};
}
});
+ const io = req.app.get('io');
+ if (io) {
+ io.emit('referralModerationUpdated', {
+ referralId: id,
+ status: referral.moderation?.status || 'visible',
+ referral
+ });
+ }
+
res.json({
message: 'Referral flagged successfully',
referral,
@@ -578,22 +569,32 @@ async function hideReferral(req, res, next) {
try {
const { id } = req.params;
const { reason } = req.body;
+ const normalizedReason = normalizeModerationReason(reason, 'Hidden by administrator');
const { referral, moderationAction } = await moderateReferral({
referralId: id,
adminId: req.user.id,
actionType: 'hide',
- reason: normalizeModerationReason(reason, 'Hidden by administrator'),
+ reason: normalizedReason,
referralMutator: async (referralDoc) => {
referralDoc.moderation = {
status: 'hidden',
- reason: normalizeModerationReason(reason, 'Hidden by administrator'),
+ reason: normalizedReason,
moderatedBy: req.user.id,
moderatedAt: new Date()
};
}
});
+ const io = req.app.get('io');
+ if (io) {
+ io.emit('referralModerationUpdated', {
+ referralId: id,
+ status: referral.moderation?.status || 'visible',
+ referral
+ });
+ }
+
res.json({
message: 'Referral hidden successfully',
referral,
@@ -611,22 +612,32 @@ async function restoreReferral(req, res, next) {
try {
const { id } = req.params;
const { reason } = req.body;
+ const normalizedReason = normalizeModerationReason(reason, 'Restored by administrator');
const { referral, moderationAction } = await moderateReferral({
referralId: id,
adminId: req.user.id,
actionType: 'restore',
- reason: normalizeModerationReason(reason, 'Restored by administrator'),
+ reason: normalizedReason,
referralMutator: async (referralDoc) => {
referralDoc.moderation = {
status: 'visible',
- reason: normalizeModerationReason(reason, 'Restored by administrator'),
+ reason: normalizedReason,
moderatedBy: req.user.id,
moderatedAt: new Date()
};
}
});
+ const io = req.app.get('io');
+ if (io) {
+ io.emit('referralModerationUpdated', {
+ referralId: id,
+ status: referral.moderation?.status || 'visible',
+ referral
+ });
+ }
+
res.json({
message: 'Referral restored successfully',
referral,
@@ -644,16 +655,17 @@ async function suspendReferralPoster(req, res, next) {
try {
const { id } = req.params;
const { reason } = req.body;
+ const normalizedReason = normalizeModerationReason(reason, 'Poster suspended by administrator');
const { referral, moderationAction } = await moderateReferral({
referralId: id,
adminId: req.user.id,
actionType: 'suspend_poster',
- reason: normalizeModerationReason(reason, 'Poster suspended by administrator'),
+ reason: normalizedReason,
referralMutator: async (referralDoc) => {
referralDoc.moderation = {
status: 'hidden',
- reason: normalizeModerationReason(reason, 'Poster suspended by administrator'),
+ reason: normalizedReason,
moderatedBy: req.user.id,
moderatedAt: new Date()
};
@@ -666,12 +678,21 @@ async function suspendReferralPoster(req, res, next) {
await User.findByIdAndUpdate(posterId, {
referralPostingSuspended: true,
referralPostingSuspendedAt: new Date(),
- referralPostingSuspendedReason: normalizeModerationReason(reason, 'Poster suspended by administrator'),
+ referralPostingSuspendedReason: normalizedReason,
referralPostingSuspendedBy: req.user.id
}, { session });
}
});
+ const io = req.app.get('io');
+ if (io) {
+ io.emit('referralModerationUpdated', {
+ referralId: id,
+ status: referral.moderation?.status || 'visible',
+ referral
+ });
+ }
+
res.json({
message: 'Referral poster suspended successfully',
referral,
@@ -688,11 +709,7 @@ async function suspendReferralPoster(req, res, next) {
async function getReferralMessages(req, res, next) {
try {
const { id } = req.params;
- const { referral, access } = await getReferralAccess(id, req.user.id);
-
- if (!referral) {
- return res.status(404).json({ message: 'Referral not found' });
- }
+ const { referral, access } = req.referralAccess;
if (!access) {
return res.status(403).json({ message: 'Access denied' });
@@ -713,19 +730,40 @@ async function sendReferralMessage(req, res, next) {
try {
const { id } = req.params;
const { body, recipientId } = req.body;
- const { referral, currentUser, access, isOwner, isApplicant } = await getReferralAccess(id, req.user.id);
- if (!referral) {
- return res.status(404).json({ message: 'Referral not found' });
+ const validationErrors = {};
+
+ // Validate referral ID (id)
+ if (!mongoose.Types.ObjectId.isValid(id)) {
+ validationErrors.id = 'Invalid referral ID format';
}
- if (!access) {
- return res.status(403).json({ message: 'Access denied' });
+ // Validate recipientId if provided
+ if (recipientId !== undefined && recipientId !== null && String(recipientId).trim() !== '') {
+ if (!mongoose.Types.ObjectId.isValid(recipientId)) {
+ validationErrors.recipientId = 'Recipient ID must be a valid ObjectId';
+ }
}
+ // Validate body
const trimmedBody = String(body || '').trim();
if (!trimmedBody) {
- return res.status(400).json({ message: 'Message body is required' });
+ validationErrors.body = 'Message body is required';
+ } else if (trimmedBody.length > 2000) {
+ validationErrors.body = 'Message body cannot exceed 2000 characters';
+ }
+
+ if (Object.keys(validationErrors).length > 0) {
+ return res.status(400).json({
+ message: 'Validation failed',
+ errors: validationErrors
+ });
+ }
+
+ const { referral, currentUser, access, isOwner, isApplicant } = req.referralAccess;
+
+ if (!access) {
+ return res.status(403).json({ message: 'Access denied' });
}
let recipientUser = null;
@@ -807,13 +845,7 @@ async function getMyReferrals(req, res, next) {
async function computeReferralBonus(req, res, next) {
try {
- const referral = await JobReferral.findById(req.params.id)
- .populate('postedBy', 'name email alumnus_bio')
- .populate('applicants.user', 'name email alumnus_bio');
-
- if (!referral) {
- return res.status(404).json({ message: 'Referral not found' });
- }
+ const { referral } = req.referralAccess;
const { referral: persistedReferral, bonus } = await recomputeAndPersistReferralBonus(referral, {
computedBy: req.user?.id || null
@@ -847,6 +879,6 @@ module.exports = {
getMyReferrals,
getReferralById,
getReferralTimeline,
- computeReferralBonus
+ computeReferralBonus,
+ checkReferralAccessMiddleware
};
-
diff --git a/backend/controllers/resumeAnalyzer.controller.js b/backend/controllers/resumeAnalyzer.controller.js
index c7985ee..692015c 100644
--- a/backend/controllers/resumeAnalyzer.controller.js
+++ b/backend/controllers/resumeAnalyzer.controller.js
@@ -104,11 +104,18 @@ function buildSkillExplanations({ resumeText = '', resumeSkills = [], requiredSk
matchedBecause.push(`resume does not include '${skillLabel}'`);
}
+ const confidence = matchedType === 'exact' ? 'exact' : (matchedType === 'partial' ? 'partial' : 'low');
+
+ if (matchedBecause.length === 0) {
+ matchedBecause.push(`no confirmation information available for '${skillLabel}'`);
+ }
+
return {
skill: skillLabel,
status,
matchedType,
matchedBecause,
+ confidence,
source,
};
});
diff --git a/backend/models/ResumeAnalysis.model.js b/backend/models/ResumeAnalysis.model.js
index de11c86..63e96bc 100644
--- a/backend/models/ResumeAnalysis.model.js
+++ b/backend/models/ResumeAnalysis.model.js
@@ -60,6 +60,11 @@ const resumeAnalysisSchema = new mongoose.Schema(
type: String,
default: 'jobRequirements',
},
+ confidence: {
+ type: String,
+ enum: ['exact', 'partial', 'low'],
+ default: 'low',
+ },
},
],
default: [],
diff --git a/backend/routes/referral.routes.js b/backend/routes/referral.routes.js
index 1fe0b72..d32acde 100644
--- a/backend/routes/referral.routes.js
+++ b/backend/routes/referral.routes.js
@@ -11,7 +11,8 @@ const {
getMyReferrals,
getReferralById,
getReferralTimeline,
- computeReferralBonus
+ computeReferralBonus,
+ checkReferralAccessMiddleware
} = require('../controllers/referral.controller');
const { authenticate, isStudent } = require('../middlewares/auth.middleware');
@@ -35,31 +36,34 @@ router.post('/', authenticate, createReferral);
// Get all referrals
router.get('/', authenticate, getReferrals);
+// Get my referrals
+router.get('/my-referrals', authenticate, getMyReferrals);
+
+// Check access for all ID-based subroutes
+router.use('/:id', authenticate, checkReferralAccessMiddleware);
+
// Apply for referral
-router.post('/:id/apply', authenticate, isStudent, applyForReferral);
+router.post('/:id/apply', isStudent, applyForReferral);
// Referral Q&A / messaging
-router.post('/:id/messages', authenticate, sendReferralMessage);
-router.get('/:id/messages', authenticate, getReferralMessages);
+router.post('/:id/messages', sendReferralMessage);
+router.get('/:id/messages', getReferralMessages);
// Referral bonus computation
-router.post('/:id/compute-bonus', authenticate, allowBonusCompute, computeReferralBonus);
+router.post('/:id/compute-bonus', allowBonusCompute, computeReferralBonus);
// Manage applicants (only poster)
-router.put('/:id/applicants/:applicantId/accept', authenticate, acceptReferral);
-router.put('/:id/applicants/:applicantId/reject', authenticate, rejectReferral);
+router.put('/:id/applicants/:applicantId/accept', acceptReferral);
+router.put('/:id/applicants/:applicantId/reject', rejectReferral);
// Close referral (poster/admin)
-router.patch('/:id/close', authenticate, closeReferral);
+router.patch('/:id/close', closeReferral);
// Get referral timeline
-router.get('/:id/timeline', authenticate, getReferralTimeline);
-
-// Get my referrals
-router.get('/my-referrals', authenticate, getMyReferrals);
+router.get('/:id/timeline', getReferralTimeline);
// Get single referral
-router.get('/:id', authenticate, getReferralById);
+router.get('/:id', getReferralById);
module.exports = router;
diff --git a/backend/utils/socket.js b/backend/utils/socket.js
index 994316b..eaa0fb6 100644
--- a/backend/utils/socket.js
+++ b/backend/utils/socket.js
@@ -316,6 +316,36 @@ const initializeSocket = (io) => {
}
});
+ // ===== REFERRAL: JOIN/LEAVE ROOMS =====
+ socket.on('joinReferral', (data) => {
+ try {
+ const { referralId } = data || {};
+ if (!referralId) {
+ socket.emit('error', { message: 'referralId is required' });
+ return;
+ }
+
+ const roomId = `referral:${referralId}`;
+ socket.join(roomId);
+ console.log(`Socket ${socket.id} joined referral room ${roomId}`);
+ } catch (error) {
+ console.error('Error in joinReferral:', error);
+ }
+ });
+
+ socket.on('leaveReferral', (data) => {
+ try {
+ const { referralId } = data || {};
+ if (!referralId) return;
+
+ const roomId = `referral:${referralId}`;
+ socket.leave(roomId);
+ console.log(`Socket ${socket.id} left referral room ${roomId}`);
+ } catch (error) {
+ console.error('Error in leaveReferral:', error);
+ }
+ });
+
// ===== DISCONNECT HANDLER =====
socket.on('disconnect', () => {
try {
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 4765d0e..e2f8809 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -4,6 +4,7 @@ import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom";
import { ToastContainer } from "react-toastify";
import { AuthProvider, useAuth } from "./AuthContext";
import { ThemeProvider } from "./ThemeContext";
+import { SocketProvider } from "./SocketContext";
import ScrollToTop from "./components/ScrollToTop";
import SmoothWheelScroll from "./components/SmoothWheelScroll";
import PrivateRoute from "./components/PrivateRoute";
@@ -77,9 +78,11 @@ function App() {
-
-
-
+
+
+
+
+
diff --git a/frontend/src/SocketContext.jsx b/frontend/src/SocketContext.jsx
index 334060b..f93edc1 100644
--- a/frontend/src/SocketContext.jsx
+++ b/frontend/src/SocketContext.jsx
@@ -98,6 +98,29 @@ export const SocketProvider = ({ children }) => {
console.log(`Left conversation: ${conversationId}`);
}, [socket]);
+ /**
+ * Join a referral room
+ */
+ const joinReferral = useCallback((referralId) => {
+ if (!socket || !isConnected) {
+ console.warn('Socket not connected');
+ return;
+ }
+
+ socket.emit('joinReferral', { referralId });
+ console.log(`Joined referral room: referral:${referralId}`);
+ }, [socket, isConnected]);
+
+ /**
+ * Leave a referral room
+ */
+ const leaveReferral = useCallback((referralId) => {
+ if (!socket) return;
+
+ socket.emit('leaveReferral', { referralId });
+ console.log(`Left referral room: referral:${referralId}`);
+ }, [socket]);
+
/**
* Mark messages as read
*/
@@ -242,6 +265,8 @@ export const SocketProvider = ({ children }) => {
typingUsers,
joinConversation,
leaveConversation,
+ joinReferral,
+ leaveReferral,
markAsRead,
addReaction,
removeReaction,
@@ -260,6 +285,8 @@ export const SocketProvider = ({ children }) => {
typingUsers,
joinConversation,
leaveConversation,
+ joinReferral,
+ leaveReferral,
markAsRead,
addReaction,
removeReaction,
@@ -289,4 +316,18 @@ export const useSocket = () => {
return context;
};
+/**
+ * Hook to listen to a socket event
+ */
+export const useSocketListener = (event, callback) => {
+ const { on } = useSocket();
+
+ useEffect(() => {
+ const cleanup = on(event, callback);
+ return () => {
+ if (cleanup) cleanup();
+ };
+ }, [event, callback, on]);
+};
+
export default SocketContext;
diff --git a/frontend/src/admin/AdminReferrals.jsx b/frontend/src/admin/AdminReferrals.jsx
index 53d9168..ffcec27 100644
--- a/frontend/src/admin/AdminReferrals.jsx
+++ b/frontend/src/admin/AdminReferrals.jsx
@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import apiClient from '../api/client';
+import { useSocketListener } from '../SocketContext';
const statusColors = {
pending: 'warning',
@@ -39,6 +40,27 @@ const AdminReferrals = () => {
const [moderationActions, setModerationActions] = useState([]);
const [auditLoading, setAuditLoading] = useState(false);
+ useSocketListener('referralModerationUpdated', (payload) => {
+ if (payload) {
+ const { referralId, status, referral: updatedReferral } = payload;
+ setModerationReferrals((currentList) =>
+ currentList.map((item) => {
+ if (item._id !== referralId) return item;
+
+ const updatedPostedBy = updatedReferral?.postedBy
+ ? { ...item.postedBy, ...updatedReferral.postedBy }
+ : item.postedBy;
+
+ return {
+ ...item,
+ postedBy: updatedPostedBy,
+ moderation: { ...item.moderation, status }
+ };
+ })
+ );
+ }
+ });
+
useEffect(() => {
fetchJobReferrals();
fetchModerationReferrals();
@@ -115,9 +137,45 @@ const AdminReferrals = () => {
return;
}
+ const trimmedReason = reason.trim() || defaultReason || '';
+
+ // Save current list state for fallback
+ const previousModerationReferrals = [...moderationReferrals];
+
+ // Optimistically update the UI
+ setModerationReferrals((currentList) =>
+ currentList.map((item) => {
+ if (item._id !== referralId) return item;
+
+ let newStatus = item.moderation?.status || 'visible';
+ let posterSuspended = item.postedBy?.referralPostingSuspended || false;
+
+ if (actionType === 'flag') {
+ newStatus = 'flagged';
+ } else if (actionType === 'hide') {
+ newStatus = 'hidden';
+ } else if (actionType === 'restore') {
+ newStatus = 'visible';
+ } else if (actionType === 'suspend-poster') {
+ newStatus = 'hidden';
+ posterSuspended = true;
+ }
+
+ return {
+ ...item,
+ postedBy: item.postedBy ? { ...item.postedBy, referralPostingSuspended: posterSuspended } : null,
+ moderation: {
+ ...item.moderation,
+ status: newStatus,
+ reason: trimmedReason
+ }
+ };
+ })
+ );
+
try {
await apiClient.post(`/admin/referrals/${referralId}/${actionType}`, {
- reason: reason.trim() || defaultReason || ''
+ reason: trimmedReason
});
await Promise.all([
@@ -127,6 +185,8 @@ const AdminReferrals = () => {
fetchModerationActions(referralId);
} catch (err) {
+ // Roll back on error
+ setModerationReferrals(previousModerationReferrals);
alert(err.response?.data?.message || 'Failed to update moderation state');
}
};
diff --git a/frontend/src/components/AnalysisResults.jsx b/frontend/src/components/AnalysisResults.jsx
index 1160013..17aa507 100644
--- a/frontend/src/components/AnalysisResults.jsx
+++ b/frontend/src/components/AnalysisResults.jsx
@@ -74,6 +74,7 @@ const SimpleBarChart = ({ matched, missing }) => {
const SkillExplanationRow = ({ item }) => {
const isMatched = item?.status === 'matched';
const matchedTypeLabel = item?.matchedType || 'none';
+ const confidence = item?.confidence || (isMatched ? (matchedTypeLabel === 'exact' ? 'exact' : 'partial') : 'low');
const toneClass = isMatched
? 'border-green-200 bg-green-50 text-green-800'
: 'border-red-200 bg-red-50 text-red-800';
@@ -96,6 +97,13 @@ const SkillExplanationRow = ({ item }) => {
{matchedTypeLabel}
+
+ {confidence.charAt(0).toUpperCase() + confidence.slice(1)} Confidence
+
@@ -119,6 +127,7 @@ const SkillExplanationsPanel = ({ analysis }) => {
skill,
status: 'matched',
matchedType: 'exact',
+ confidence: 'exact',
matchedBecause: [`Analysis reported '${skill}' as matched.`],
source: 'analysis',
})),
@@ -126,6 +135,7 @@ const SkillExplanationsPanel = ({ analysis }) => {
skill,
status: 'missing',
matchedType: 'none',
+ confidence: 'low',
matchedBecause: [`Analysis reported '${skill}' as missing.`],
source: 'analysis',
})),
diff --git a/frontend/src/components/ReferralDetail.jsx b/frontend/src/components/ReferralDetail.jsx
index 31c74f8..038fd47 100644
--- a/frontend/src/components/ReferralDetail.jsx
+++ b/frontend/src/components/ReferralDetail.jsx
@@ -3,12 +3,36 @@ import { useParams, Link, useNavigate } from 'react-router-dom';
import apiClient from '../api/client';
import { useAuth } from '../AuthContext';
import { toast } from 'react-toastify';
+import { useSocket, useSocketListener } from '../SocketContext';
const ReferralDetail = () => {
const { id } = useParams();
const navigate = useNavigate();
const { isAuthReady, user: authUser } = useAuth();
const [referral, setReferral] = useState(null);
+
+ const { joinReferral, leaveReferral } = useSocket();
+
+ useEffect(() => {
+ if (id) {
+ joinReferral(id);
+ return () => {
+ leaveReferral(id);
+ };
+ }
+ }, [id, joinReferral, leaveReferral]);
+
+ useSocketListener('referralMessageCreated', (payload) => {
+ if (payload && String(payload.referralId) === String(id)) {
+ setMessages((prevMessages) => {
+ const messageExists = prevMessages.some((msg) => msg._id === payload.message?._id);
+ if (messageExists) {
+ return prevMessages;
+ }
+ return [...prevMessages, payload.message];
+ });
+ }
+ });
const [timeline, setTimeline] = useState([]);
const [messages, setMessages] = useState([]);
const [messageBody, setMessageBody] = useState('');
diff --git a/frontend/src/components/ReferralList.jsx b/frontend/src/components/ReferralList.jsx
index d60396b..7134119 100644
--- a/frontend/src/components/ReferralList.jsx
+++ b/frontend/src/components/ReferralList.jsx
@@ -4,10 +4,29 @@ import { FaBookmark, FaRegBookmark } from 'react-icons/fa';
import apiClient from '../api/client';
import { useAuth } from '../AuthContext';
import { toast } from 'react-toastify';
+import { useSocketListener } from '../SocketContext';
const ReferralList = () => {
const { user } = useAuth();
const [referrals, setReferrals] = useState([]);
+
+ useSocketListener('referralModerationUpdated', (payload) => {
+ if (payload) {
+ const { referralId, status } = payload;
+ if (status === 'hidden' || status === 'removed') {
+ setReferrals((prevReferrals) => prevReferrals.filter(r => r._id !== referralId));
+ } else {
+ setReferrals((prevReferrals) =>
+ prevReferrals.map((r) =>
+ r._id === referralId
+ ? { ...r, moderation: { ...r.moderation, status } }
+ : r
+ )
+ );
+ }
+ }
+ });
+
const [pagination, setPagination] = useState({ total: 0, page: 1, limit: 10 });
const [loading, setLoading] = useState(true);
const [loadingNextPage, setLoadingNextPage] = useState(false);