Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 5 additions & 49 deletions client/src/components/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]);

Expand Down
21 changes: 19 additions & 2 deletions server/app/controllers/assignmentController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -144,14 +151,19 @@ 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,
studentId,
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) {
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand Down
14 changes: 9 additions & 5 deletions server/app/controllers/enrollmentController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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,
});
Expand Down
18 changes: 17 additions & 1 deletion server/app/controllers/liveClassController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -32,17 +33,24 @@ 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,
courseId,
scheduledAt: liveClass.scheduledAt,
type: liveClass.type,
});
// Persist + push notification:new
pushNotification(
studentId.toString(),
`📹 Live class scheduled: "${liveClass.title}"`,
"course"
);
});
} catch {
/* non-critical */
Expand Down Expand Up @@ -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)
Expand Down
19 changes: 19 additions & 0 deletions server/app/controllers/materialController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
14 changes: 13 additions & 1 deletion server/app/controllers/quizController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand Down
23 changes: 23 additions & 0 deletions server/app/services/notificationService.js
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading