diff --git a/backend/controllers/referral.controller.js b/backend/controllers/referral.controller.js index f2bf630..bc9687d 100644 --- a/backend/controllers/referral.controller.js +++ b/backend/controllers/referral.controller.js @@ -186,7 +186,11 @@ async function awardFirstReferralBadge(userId) { async function getReferrals(req, res, next) { try { const { status = 'open', search, page = 1, limit = 10 } = req.query; - const skip = (page - 1) * limit; + + const pageNum = Math.max(1, Number(page)); + const limitNum = Math.max(1, Number(limit)); + const skip = (pageNum - 1) * limitNum; + const currentUser = req.user?.id ? await getCurrentUserSummary(req.user.id) : null; const query = { status }; @@ -201,20 +205,21 @@ async function getReferrals(req, res, next) { .populate('postedBy', 'name email alumnus_bio type referralPostingSuspended referralPostingSuspendedReason') .populate('applicants.user', 'name email') .sort({ createdAt: -1 }) - .skip(skip * 1) - .limit(limit * 1); + .skip(skip) + .limit(limitNum); const total = await JobReferral.countDocuments(query); res.json({ referrals, - pagination: { total, page: parseInt(page), limit: parseInt(limit) } + pagination: { total, page: pageNum, limit: limitNum } }); } catch (err) { next(err); } } + // Apply for a referral async function applyForReferral(req, res, next) { try { diff --git a/backend/controllers/resumeAnalyzer.controller.js b/backend/controllers/resumeAnalyzer.controller.js index da81c6f..c7985ee 100644 --- a/backend/controllers/resumeAnalyzer.controller.js +++ b/backend/controllers/resumeAnalyzer.controller.js @@ -234,37 +234,114 @@ async function analyzeResume(req, res, next) { const missingSkills = skillExplanations.filter((entry) => entry.status === 'missing').map((entry) => entry.skill); // Course suggestions: match missing skills against course tags/title (best-effort) - // We still return generic recommendations even if no matching courses are found. + // Fix: avoid N+1 DB queries by doing a single batched query for all missing skills (up to 8). const missingTop = missingSkills.slice(0, 8); const courseSuggestions = []; if (missingTop.length > 0) { - for (const skill of missingTop) { + const escapeRegExp = (value) => String(value ?? '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + const skillRegexes = missingTop + .map((skill) => ({ + skill: String(skill).trim(), + regex: new RegExp(escapeRegExp(String(skill)), 'i'), + })) + .filter((x) => x.skill.length > 0); + + if (skillRegexes.length > 0) { + // Single query that returns candidates matched by ANY missing skill. + const orConditions = [ + ...skillRegexes.map((sr) => ({ title: { $regex: sr.regex } })), + ...skillRegexes.map((sr) => ({ + tags: { $elemMatch: { $regex: sr.regex } }, + })), + ...skillRegexes.map((sr) => ({ description: { $regex: sr.regex } })), + ]; + const courses = await Course.find({ isPublished: true, - $or: [ - { title: { $regex: new RegExp(skill, 'i') } }, - { tags: { $elemMatch: { $regex: new RegExp(skill, 'i') } } }, - { description: { $regex: new RegExp(skill, 'i') } }, - ], + $or: orConditions, }) - .limit(3) + // Upper bound; we post-process to pick best matches. + .limit(60) .lean(); if (courses && courses.length > 0) { + const getSkillScoreForCourse = (course, skillEntry) => { + // Important: RegExp#test is stateful for /g. We never use /g, but cloning keeps it safe. + const regex = new RegExp(skillEntry.regex.source, skillEntry.regex.flags); + + const title = String(course?.title || ''); + const description = String(course?.description || ''); + const tags = Array.isArray(course?.tags) ? course.tags : []; + + let score = 0; + + if (regex.test(title)) score += 6; + if (regex.test(description)) score += 3; + if (tags.some((t) => regex.test(String(t)))) score += 5; + + const skillLower = String(skillEntry.skill).toLowerCase().trim(); + if (skillLower && title.toLowerCase().includes(skillLower)) score += 2; + + return score; + }; + + + for (const c of courses) { + let bestSkill = null; + let bestScore = 0; + + for (const sr of skillRegexes) { + const s = getSkillScoreForCourse(c, sr); + if (s > bestScore) { + bestScore = s; + bestSkill = sr.skill; + } + } + + if (bestSkill && bestScore > 0) { + courseSuggestions.push({ + skill: bestSkill, + courseId: c._id, + title: c.title, + category: c.category, + _score: bestScore, + }); + } + } + + courseSuggestions.sort((a, b) => (b._score || 0) - (a._score || 0)); + + const seen = new Set(); + const deduped = []; + for (const cs of courseSuggestions) { + const key = String(cs.courseId); + if (seen.has(key)) continue; + seen.add(key); + deduped.push(cs); + } + + courseSuggestions.length = 0; courseSuggestions.push( - ...courses.map((c) => ({ - skill, - courseId: c._id, - title: c.title, - category: c.category, + ...deduped.map((cs) => ({ + skill: cs.skill, + courseId: cs.courseId, + title: cs.title, + category: cs.category, })) ); } } } + + + + + const recommendations = buildRecommendations({ missingSkills, score }); + if (courseSuggestions.length > 0) { recommendations.push('Suggested courses based on your skill gaps:'); for (const cs of courseSuggestions.slice(0, 6)) { diff --git a/backend/server.js b/backend/server.js index a9503df..c3a6dba 100644 --- a/backend/server.js +++ b/backend/server.js @@ -44,7 +44,7 @@ connectDB(); ========================= */ const normalizeOrigin = (origin = '') => - origin.trim().replace(/\/+$/, ''); + String(origin).trim().replace(/\/+$/, ''); const DEFAULT_ORIGINS = [ 'http://localhost:5173', @@ -52,23 +52,31 @@ const DEFAULT_ORIGINS = [ 'https://alumni-management-system-xi.vercel.app' ]; -const ENV_ORIGINS = [ - process.env.FRONTEND_URL, - ...(process.env.FRONTEND_URLS || '') - .split(',') +const parseOriginsEnv = () => { + const single = process.env.FRONTEND_URL; + const multipleRaw = process.env.FRONTEND_URLS; + + const multiple = typeof multipleRaw === 'string' + ? multipleRaw.split(',') + : []; + + return [single, ...multiple] .map((origin) => normalizeOrigin(origin)) - .filter(Boolean) -]; + .filter(Boolean); +}; const CLIENT_ORIGINS = [...new Set( - [...DEFAULT_ORIGINS, ...ENV_ORIGINS] + [...DEFAULT_ORIGINS, ...parseOriginsEnv()] .map((origin) => normalizeOrigin(origin)) .filter(Boolean) )]; -console.log('Allowed CORS origins:', CLIENT_ORIGINS); +if (process.env.NODE_ENV === 'development') { + console.log('Allowed CORS origins:', CLIENT_ORIGINS); +} logger.logInfo('Server initialized with logging system enabled'); + const corsOptions = { origin: (origin, callback) => { // Allow requests without origin (Postman, curl, health checks) @@ -201,7 +209,18 @@ const server = http.createServer(app); // Initialize Socket.IO with CORS configuration const io = socketIO(server, { cors: { - origin: CLIENT_ORIGINS, + origin: (origin, callback) => { + // Socket.IO needs exact origin match; normalize to reduce intermittent failures + if (!origin) return callback(null, true); + + const normalizedOrigin = normalizeOrigin(origin); + + if (CLIENT_ORIGINS.includes(normalizedOrigin)) { + return callback(null, true); + } + + return callback(new Error(`Socket CORS blocked for origin: ${origin}`)); + }, methods: ['GET', 'POST'], credentials: true }, diff --git a/frontend/src/components/ReferralList.jsx b/frontend/src/components/ReferralList.jsx index dbece7b..d60396b 100644 --- a/frontend/src/components/ReferralList.jsx +++ b/frontend/src/components/ReferralList.jsx @@ -10,6 +10,7 @@ const ReferralList = () => { const [referrals, setReferrals] = useState([]); const [pagination, setPagination] = useState({ total: 0, page: 1, limit: 10 }); const [loading, setLoading] = useState(true); + const [loadingNextPage, setLoadingNextPage] = useState(false); const [error, setError] = useState(''); const [search, setSearch] = useState(''); const [statusFilter, setStatusFilter] = useState('open'); @@ -17,6 +18,7 @@ const ReferralList = () => { const [savedItems, setSavedItems] = useState([]); const limit = 10; + useEffect(() => { // Fetch whenever filter/search/page changes. // Pagination state is always synced from the server inside fetchReferrals(). @@ -27,6 +29,7 @@ const ReferralList = () => { + useEffect(() => { if (!user) { setSavedItems([]); @@ -51,11 +54,17 @@ const ReferralList = () => { }, [user]); const fetchReferrals = async ({ page: requestedPage, search: requestedSearch } = {}) => { + const requestedPageNum = Number(requestedPage ?? 1); + const isPageNavigation = requestedPageNum !== Number(pagination?.page); try { - setLoading(true); - setError(''); + if (isPageNavigation) { + setLoadingNextPage(true); + } else { + setLoading(true); + } + setError(''); const params = { status: statusFilter, @@ -63,7 +72,6 @@ const ReferralList = () => { limit }; - const effectiveSearch = requestedSearch ?? ''; if (effectiveSearch) params.search = effectiveSearch; @@ -82,9 +90,11 @@ const ReferralList = () => { toast.error('Failed to load referrals'); } finally { setLoading(false); + setLoadingNextPage(false); } }; + const handleSearch = (e) => { e.preventDefault(); setPage(1); @@ -100,6 +110,7 @@ const ReferralList = () => { ); } + const savedMap = savedItems.reduce((accumulator, item) => { accumulator[`${item.entityType}:${item.entityId}`] = item; return accumulator; @@ -286,24 +297,31 @@ const ReferralList = () => { {/* Pagination */} {pagination.total > 0 && (
-
+
+ - Page {page} + + Page {page} + {loadingNextPage && (Loading…)} + +