diff --git a/client/src/components/Navbar.jsx b/client/src/components/Navbar.jsx index 39557a3..df749f5 100644 --- a/client/src/components/Navbar.jsx +++ b/client/src/components/Navbar.jsx @@ -92,58 +92,14 @@ function Navbar({ showBack }) { .then((d) => Array.isArray(d) && setNotifs(d)); const socket = getSocket(user.id); - socket.on("new-course", (notif) => { - setNotifs((prev) => [ - { - id: notif.id || Date.now(), - message: notif.message, - createdAt: notif.createdAt, - read: false, - }, - ...prev, - ]); - }); - socket.on("live-class-scheduled", (data) => { - setNotifs((prev) => [ - { - id: `lc_${Date.now()}`, - message: `📹 Live class scheduled: "${data.title}"`, - createdAt: new Date().toISOString(), - read: false, - }, - ...prev, - ]); - }); - socket.on("live-class-status", (data) => { - if (data.status === "live") { - setNotifs((prev) => [ - { - id: `lcs_${Date.now()}`, - message: `🔴 A live class just started!`, - createdAt: new Date().toISOString(), - read: false, - }, - ...prev, - ]); - } - }); - socket.on("student-enrolled", (data) => { - setNotifs((prev) => [ - { - id: `enroll_${Date.now()}`, - message: data.message || "A student enrolled in your course", - createdAt: new Date().toISOString(), - read: false, - }, - ...prev, - ]); + + // Single unified listener — backend persists to DB before emitting + socket.on("notification:new", (notif) => { + setNotifs((prev) => [{ ...notif, read: false }, ...prev]); }); return () => { - socket.off("new-course"); - socket.off("live-class-scheduled"); - socket.off("live-class-status"); - socket.off("student-enrolled"); + socket.off("notification:new"); }; }, [user?.id, isAuthenticated]); diff --git a/server/app/controllers/assignmentController.js b/server/app/controllers/assignmentController.js index 12d65e0..355f37d 100644 --- a/server/app/controllers/assignmentController.js +++ b/server/app/controllers/assignmentController.js @@ -2,6 +2,7 @@ import Assignment from "../models/Assignment.js"; import Submission from "../models/Submission.js"; import Course from "../models/Course.js"; import { emitToCourse, emitToUser } from "../services/socketService.js"; +import { pushNotification } from "../services/notificationService.js"; // ─── POST /api/courses/:courseId/assignments ────────────────────────────────── export async function createAssignment(req, res) { @@ -29,6 +30,12 @@ export async function createAssignment(req, res) { const formatted = formatAssignment(assignment); emitToCourse(courseId, "assignment:new", formatted); + + // Notify every enrolled student + course.enrolledStudents.forEach((studentId) => { + pushNotification(studentId.toString(), `📝 New assignment: "${title}"`, "course"); + }); + res.status(201).json(formatted); } catch (err) { console.error("createAssignment error:", err); @@ -144,7 +151,7 @@ export async function submitAssignment(req, res) { { upsert: true, new: true } ); - // Notify teacher in real-time + // Notify teacher in real-time (socket) + persist notification emitToUser(course.teacher.toString(), "assignment:submitted", { assignmentId: id, assignmentTitle: assignment.title, @@ -152,6 +159,11 @@ export async function submitAssignment(req, res) { courseId: assignment.course.toString(), submittedAt: now, }); + pushNotification( + course.teacher.toString(), + `📤 A student submitted assignment: "${assignment.title}"`, + "course" + ); res.status(201).json(formatSubmission(submission)); } catch (err) { @@ -218,7 +230,7 @@ export async function gradeSubmission(req, res) { submission.status = "graded"; await submission.save(); - // Notify the student of their grade + // Notify the student of their grade (socket) + persist notification emitToUser(submission.student.toString(), "assignment:graded", { assignmentId: submission.assignment._id.toString(), assignmentTitle: submission.assignment.title, @@ -227,6 +239,11 @@ export async function gradeSubmission(req, res) { feedback: submission.feedback, maxScore: submission.assignment.maxScore, }); + pushNotification( + submission.student.toString(), + `✅ Your assignment "${submission.assignment.title}" was graded: ${submission.score}/${submission.assignment.maxScore}`, + "course" + ); res.json(formatSubmission(submission)); } catch (err) { diff --git a/server/app/controllers/enrollmentController.js b/server/app/controllers/enrollmentController.js index 98c0b89..98975ef 100644 --- a/server/app/controllers/enrollmentController.js +++ b/server/app/controllers/enrollmentController.js @@ -2,8 +2,8 @@ import Enrollment from "../models/Enrollment.js"; import Course from "../models/Course.js"; import Material from "../models/Material.js"; import CompletedMaterial from "../models/CompletedMaterial.js"; -import Notification from "../models/Notification.js"; import { getIO } from "../services/socketService.js"; +import { pushNotification } from "../services/notificationService.js"; // ─── POST /api/enrollments ──────────────────────────────────────────────────── export async function enroll(req, res) { @@ -30,13 +30,17 @@ export async function enroll(req, res) { { upsert: true, new: true } ); - // Notify the teacher + // Notify the teacher (persists to DB + emits notification:new) + pushNotification( + course.teacher.toString(), + `🎓 A new student enrolled in "${course.title}"`, + "course" + ); + // Keep student-enrolled for StudentDashboard / other listeners try { - const notifMessage = `A new student enrolled in "${course.title}"`; - await Notification.create({ user: course.teacher, message: notifMessage, type: "course" }); const io = getIO(); io.to(`user:${course.teacher}`).emit("student-enrolled", { - message: notifMessage, + message: `A new student enrolled in "${course.title}"`, courseId, studentId, }); diff --git a/server/app/controllers/liveClassController.js b/server/app/controllers/liveClassController.js index 99e89f6..b25d9bd 100644 --- a/server/app/controllers/liveClassController.js +++ b/server/app/controllers/liveClassController.js @@ -3,6 +3,7 @@ import ClassComment from "../models/ClassComment.js"; import ClassQuestion from "../models/ClassQuestion.js"; import Course from "../models/Course.js"; import { getIO } from "../services/socketService.js"; +import { pushNotification } from "../services/notificationService.js"; // ─── POST /api/courses/:courseId/live-classes ───────────────────────────────── export async function createLiveClass(req, res) { @@ -32,10 +33,11 @@ export async function createLiveClass(req, res) { meetingLink: classType === "meetLink" ? meetingLink || "" : "", }); - // Notify enrolled students via socket + // Notify enrolled students via socket + persist notification try { const io = getIO(); course.enrolledStudents.forEach((studentId) => { + // Keep live-class-scheduled for StudentDashboard reload io.to(`user:${studentId}`).emit("live-class-scheduled", { liveClassId: liveClass._id, title: liveClass.title, @@ -43,6 +45,12 @@ export async function createLiveClass(req, res) { scheduledAt: liveClass.scheduledAt, type: liveClass.type, }); + // Persist + push notification:new + pushNotification( + studentId.toString(), + `📹 Live class scheduled: "${liveClass.title}"`, + "course" + ); }); } catch { /* non-critical */ @@ -170,6 +178,14 @@ export async function updateLiveClassStatus(req, res) { status, type: liveClass.type, }); + // Persist notification when class goes live + if (status === "live") { + pushNotification( + studentId.toString(), + `🔴 Live class started: "${liveClass.title}"`, + "course" + ); + } }); // Also broadcast inside the live class room (for participants currently in it) diff --git a/server/app/controllers/materialController.js b/server/app/controllers/materialController.js index 4f1a26f..490236a 100644 --- a/server/app/controllers/materialController.js +++ b/server/app/controllers/materialController.js @@ -4,6 +4,7 @@ import CompletedMaterial from "../models/CompletedMaterial.js"; import Enrollment from "../models/Enrollment.js"; import { uploadToCloudinary, getResourceType } from "../utils/cloudinary.js"; import { emitToCourse, emitToUser } from "../services/socketService.js"; +import { pushNotification } from "../services/notificationService.js"; // ─── POST /api/courses/:courseId/materials/upload ──────────────────────────── export async function uploadMaterialFile(req, res) { @@ -42,6 +43,12 @@ export async function uploadMaterialFile(req, res) { const formatted = formatMaterial(material); emitToCourse(courseId, "material:new", formatted); + + // Notify every enrolled student + course.enrolledStudents.forEach((studentId) => { + pushNotification(studentId.toString(), `📎 New material added: "${title}"`, "course"); + }); + res.status(201).json(formatted); } catch (err) { console.error("uploadMaterialFile error:", err); @@ -75,6 +82,12 @@ export async function addMaterial(req, res) { const formatted = formatMaterial(material); emitToCourse(courseId, "material:new", formatted); + + // Notify every enrolled student + course.enrolledStudents.forEach((studentId) => { + pushNotification(studentId.toString(), `📎 New material added: "${title}"`, "course"); + }); + res.status(201).json(formatted); } catch (err) { console.error("addMaterial error:", err); @@ -124,6 +137,12 @@ export async function updateMaterial(req, res) { const formatted = formatMaterial(material); emitToCourse(courseId, "material:updated", formatted); + + // Notify every enrolled student that content was updated + course.enrolledStudents.forEach((studentId) => { + pushNotification(studentId.toString(), `✏️ Material updated: "${material.title}"`, "course"); + }); + res.json(formatted); } catch (err) { console.error("updateMaterial error:", err); diff --git a/server/app/controllers/quizController.js b/server/app/controllers/quizController.js index 12e7ccf..daf67fc 100644 --- a/server/app/controllers/quizController.js +++ b/server/app/controllers/quizController.js @@ -2,6 +2,7 @@ import Quiz from "../models/Quiz.js"; import QuizResult from "../models/QuizResult.js"; import Course from "../models/Course.js"; import { emitToCourse, emitToUser } from "../services/socketService.js"; +import { pushNotification } from "../services/notificationService.js"; // ─── POST /api/courses/:courseId/quizzes ────────────────────────────────────── export async function createQuiz(req, res) { @@ -30,6 +31,12 @@ export async function createQuiz(req, res) { const formatted = formatQuiz(quiz); emitToCourse(courseId, "quiz:new", formatted); + + // Notify every enrolled student + course.enrolledStudents.forEach((studentId) => { + pushNotification(studentId.toString(), `📊 New quiz: "${title}"`, "course"); + }); + res.status(201).json(formatted); } catch (err) { console.error("createQuiz error:", err); @@ -151,7 +158,7 @@ export async function submitQuiz(req, res) { const formatted = formatResult(result); - // Notify teacher of new submission + // Notify teacher of new submission (socket) + persist notification emitToUser(course.teacher.toString(), "quiz:submitted", { quizId: id, quizTitle: quiz.title, @@ -160,6 +167,11 @@ export async function submitQuiz(req, res) { score, totalPoints, }); + pushNotification( + course.teacher.toString(), + `📊 A student submitted quiz "${quiz.title}" — score: ${score}/${totalPoints}`, + "course" + ); res.status(201).json(formatted); } catch (err) { diff --git a/server/app/services/notificationService.js b/server/app/services/notificationService.js new file mode 100644 index 0000000..3ab9ffd --- /dev/null +++ b/server/app/services/notificationService.js @@ -0,0 +1,23 @@ +import Notification from "../models/Notification.js"; +import { emitToUser } from "./socketService.js"; + +/** + * Persist a notification to MongoDB and push it in real-time to the user's + * personal socket room via the `notification:new` event. + * + * Always fire-and-forget (never throws) — callers don't need try/catch. + */ +export async function pushNotification(userId, message, type = "course") { + try { + const notif = await Notification.create({ user: userId, message, type }); + emitToUser(userId.toString(), "notification:new", { + id: notif._id, + message: notif.message, + type: notif.type, + read: notif.read, + createdAt: notif.createdAt, + }); + } catch { + // non-critical — never crash the main request + } +}