Skip to content

Commit ced6baf

Browse files
authored
Merge pull request #37 from adity1raut/aditya-side
feat: implement notification system for course events and updates
2 parents c15eae1 + fc79a16 commit ced6baf

7 files changed

Lines changed: 105 additions & 58 deletions

File tree

client/src/components/Navbar.jsx

Lines changed: 5 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -92,58 +92,14 @@ function Navbar({ showBack }) {
9292
.then((d) => Array.isArray(d) && setNotifs(d));
9393

9494
const socket = getSocket(user.id);
95-
socket.on("new-course", (notif) => {
96-
setNotifs((prev) => [
97-
{
98-
id: notif.id || Date.now(),
99-
message: notif.message,
100-
createdAt: notif.createdAt,
101-
read: false,
102-
},
103-
...prev,
104-
]);
105-
});
106-
socket.on("live-class-scheduled", (data) => {
107-
setNotifs((prev) => [
108-
{
109-
id: `lc_${Date.now()}`,
110-
message: `📹 Live class scheduled: "${data.title}"`,
111-
createdAt: new Date().toISOString(),
112-
read: false,
113-
},
114-
...prev,
115-
]);
116-
});
117-
socket.on("live-class-status", (data) => {
118-
if (data.status === "live") {
119-
setNotifs((prev) => [
120-
{
121-
id: `lcs_${Date.now()}`,
122-
message: `🔴 A live class just started!`,
123-
createdAt: new Date().toISOString(),
124-
read: false,
125-
},
126-
...prev,
127-
]);
128-
}
129-
});
130-
socket.on("student-enrolled", (data) => {
131-
setNotifs((prev) => [
132-
{
133-
id: `enroll_${Date.now()}`,
134-
message: data.message || "A student enrolled in your course",
135-
createdAt: new Date().toISOString(),
136-
read: false,
137-
},
138-
...prev,
139-
]);
95+
96+
// Single unified listener — backend persists to DB before emitting
97+
socket.on("notification:new", (notif) => {
98+
setNotifs((prev) => [{ ...notif, read: false }, ...prev]);
14099
});
141100

142101
return () => {
143-
socket.off("new-course");
144-
socket.off("live-class-scheduled");
145-
socket.off("live-class-status");
146-
socket.off("student-enrolled");
102+
socket.off("notification:new");
147103
};
148104
}, [user?.id, isAuthenticated]);
149105

server/app/controllers/assignmentController.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Assignment from "../models/Assignment.js";
22
import Submission from "../models/Submission.js";
33
import Course from "../models/Course.js";
44
import { emitToCourse, emitToUser } from "../services/socketService.js";
5+
import { pushNotification } from "../services/notificationService.js";
56

67
// ─── POST /api/courses/:courseId/assignments ──────────────────────────────────
78
export async function createAssignment(req, res) {
@@ -29,6 +30,12 @@ export async function createAssignment(req, res) {
2930

3031
const formatted = formatAssignment(assignment);
3132
emitToCourse(courseId, "assignment:new", formatted);
33+
34+
// Notify every enrolled student
35+
course.enrolledStudents.forEach((studentId) => {
36+
pushNotification(studentId.toString(), `📝 New assignment: "${title}"`, "course");
37+
});
38+
3239
res.status(201).json(formatted);
3340
} catch (err) {
3441
console.error("createAssignment error:", err);
@@ -144,14 +151,19 @@ export async function submitAssignment(req, res) {
144151
{ upsert: true, new: true }
145152
);
146153

147-
// Notify teacher in real-time
154+
// Notify teacher in real-time (socket) + persist notification
148155
emitToUser(course.teacher.toString(), "assignment:submitted", {
149156
assignmentId: id,
150157
assignmentTitle: assignment.title,
151158
studentId,
152159
courseId: assignment.course.toString(),
153160
submittedAt: now,
154161
});
162+
pushNotification(
163+
course.teacher.toString(),
164+
`📤 A student submitted assignment: "${assignment.title}"`,
165+
"course"
166+
);
155167

156168
res.status(201).json(formatSubmission(submission));
157169
} catch (err) {
@@ -218,7 +230,7 @@ export async function gradeSubmission(req, res) {
218230
submission.status = "graded";
219231
await submission.save();
220232

221-
// Notify the student of their grade
233+
// Notify the student of their grade (socket) + persist notification
222234
emitToUser(submission.student.toString(), "assignment:graded", {
223235
assignmentId: submission.assignment._id.toString(),
224236
assignmentTitle: submission.assignment.title,
@@ -227,6 +239,11 @@ export async function gradeSubmission(req, res) {
227239
feedback: submission.feedback,
228240
maxScore: submission.assignment.maxScore,
229241
});
242+
pushNotification(
243+
submission.student.toString(),
244+
`✅ Your assignment "${submission.assignment.title}" was graded: ${submission.score}/${submission.assignment.maxScore}`,
245+
"course"
246+
);
230247

231248
res.json(formatSubmission(submission));
232249
} catch (err) {

server/app/controllers/enrollmentController.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import Enrollment from "../models/Enrollment.js";
22
import Course from "../models/Course.js";
33
import Material from "../models/Material.js";
44
import CompletedMaterial from "../models/CompletedMaterial.js";
5-
import Notification from "../models/Notification.js";
65
import { getIO } from "../services/socketService.js";
6+
import { pushNotification } from "../services/notificationService.js";
77

88
// ─── POST /api/enrollments ────────────────────────────────────────────────────
99
export async function enroll(req, res) {
@@ -30,13 +30,17 @@ export async function enroll(req, res) {
3030
{ upsert: true, new: true }
3131
);
3232

33-
// Notify the teacher
33+
// Notify the teacher (persists to DB + emits notification:new)
34+
pushNotification(
35+
course.teacher.toString(),
36+
`🎓 A new student enrolled in "${course.title}"`,
37+
"course"
38+
);
39+
// Keep student-enrolled for StudentDashboard / other listeners
3440
try {
35-
const notifMessage = `A new student enrolled in "${course.title}"`;
36-
await Notification.create({ user: course.teacher, message: notifMessage, type: "course" });
3741
const io = getIO();
3842
io.to(`user:${course.teacher}`).emit("student-enrolled", {
39-
message: notifMessage,
43+
message: `A new student enrolled in "${course.title}"`,
4044
courseId,
4145
studentId,
4246
});

server/app/controllers/liveClassController.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import ClassComment from "../models/ClassComment.js";
33
import ClassQuestion from "../models/ClassQuestion.js";
44
import Course from "../models/Course.js";
55
import { getIO } from "../services/socketService.js";
6+
import { pushNotification } from "../services/notificationService.js";
67

78
// ─── POST /api/courses/:courseId/live-classes ─────────────────────────────────
89
export async function createLiveClass(req, res) {
@@ -32,17 +33,24 @@ export async function createLiveClass(req, res) {
3233
meetingLink: classType === "meetLink" ? meetingLink || "" : "",
3334
});
3435

35-
// Notify enrolled students via socket
36+
// Notify enrolled students via socket + persist notification
3637
try {
3738
const io = getIO();
3839
course.enrolledStudents.forEach((studentId) => {
40+
// Keep live-class-scheduled for StudentDashboard reload
3941
io.to(`user:${studentId}`).emit("live-class-scheduled", {
4042
liveClassId: liveClass._id,
4143
title: liveClass.title,
4244
courseId,
4345
scheduledAt: liveClass.scheduledAt,
4446
type: liveClass.type,
4547
});
48+
// Persist + push notification:new
49+
pushNotification(
50+
studentId.toString(),
51+
`📹 Live class scheduled: "${liveClass.title}"`,
52+
"course"
53+
);
4654
});
4755
} catch {
4856
/* non-critical */
@@ -170,6 +178,14 @@ export async function updateLiveClassStatus(req, res) {
170178
status,
171179
type: liveClass.type,
172180
});
181+
// Persist notification when class goes live
182+
if (status === "live") {
183+
pushNotification(
184+
studentId.toString(),
185+
`🔴 Live class started: "${liveClass.title}"`,
186+
"course"
187+
);
188+
}
173189
});
174190

175191
// Also broadcast inside the live class room (for participants currently in it)

server/app/controllers/materialController.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import CompletedMaterial from "../models/CompletedMaterial.js";
44
import Enrollment from "../models/Enrollment.js";
55
import { uploadToCloudinary, getResourceType } from "../utils/cloudinary.js";
66
import { emitToCourse, emitToUser } from "../services/socketService.js";
7+
import { pushNotification } from "../services/notificationService.js";
78

89
// ─── POST /api/courses/:courseId/materials/upload ────────────────────────────
910
export async function uploadMaterialFile(req, res) {
@@ -42,6 +43,12 @@ export async function uploadMaterialFile(req, res) {
4243

4344
const formatted = formatMaterial(material);
4445
emitToCourse(courseId, "material:new", formatted);
46+
47+
// Notify every enrolled student
48+
course.enrolledStudents.forEach((studentId) => {
49+
pushNotification(studentId.toString(), `📎 New material added: "${title}"`, "course");
50+
});
51+
4552
res.status(201).json(formatted);
4653
} catch (err) {
4754
console.error("uploadMaterialFile error:", err);
@@ -75,6 +82,12 @@ export async function addMaterial(req, res) {
7582

7683
const formatted = formatMaterial(material);
7784
emitToCourse(courseId, "material:new", formatted);
85+
86+
// Notify every enrolled student
87+
course.enrolledStudents.forEach((studentId) => {
88+
pushNotification(studentId.toString(), `📎 New material added: "${title}"`, "course");
89+
});
90+
7891
res.status(201).json(formatted);
7992
} catch (err) {
8093
console.error("addMaterial error:", err);
@@ -124,6 +137,12 @@ export async function updateMaterial(req, res) {
124137

125138
const formatted = formatMaterial(material);
126139
emitToCourse(courseId, "material:updated", formatted);
140+
141+
// Notify every enrolled student that content was updated
142+
course.enrolledStudents.forEach((studentId) => {
143+
pushNotification(studentId.toString(), `✏️ Material updated: "${material.title}"`, "course");
144+
});
145+
127146
res.json(formatted);
128147
} catch (err) {
129148
console.error("updateMaterial error:", err);

server/app/controllers/quizController.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Quiz from "../models/Quiz.js";
22
import QuizResult from "../models/QuizResult.js";
33
import Course from "../models/Course.js";
44
import { emitToCourse, emitToUser } from "../services/socketService.js";
5+
import { pushNotification } from "../services/notificationService.js";
56

67
// ─── POST /api/courses/:courseId/quizzes ──────────────────────────────────────
78
export async function createQuiz(req, res) {
@@ -30,6 +31,12 @@ export async function createQuiz(req, res) {
3031

3132
const formatted = formatQuiz(quiz);
3233
emitToCourse(courseId, "quiz:new", formatted);
34+
35+
// Notify every enrolled student
36+
course.enrolledStudents.forEach((studentId) => {
37+
pushNotification(studentId.toString(), `📊 New quiz: "${title}"`, "course");
38+
});
39+
3340
res.status(201).json(formatted);
3441
} catch (err) {
3542
console.error("createQuiz error:", err);
@@ -151,7 +158,7 @@ export async function submitQuiz(req, res) {
151158

152159
const formatted = formatResult(result);
153160

154-
// Notify teacher of new submission
161+
// Notify teacher of new submission (socket) + persist notification
155162
emitToUser(course.teacher.toString(), "quiz:submitted", {
156163
quizId: id,
157164
quizTitle: quiz.title,
@@ -160,6 +167,11 @@ export async function submitQuiz(req, res) {
160167
score,
161168
totalPoints,
162169
});
170+
pushNotification(
171+
course.teacher.toString(),
172+
`📊 A student submitted quiz "${quiz.title}" — score: ${score}/${totalPoints}`,
173+
"course"
174+
);
163175

164176
res.status(201).json(formatted);
165177
} catch (err) {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import Notification from "../models/Notification.js";
2+
import { emitToUser } from "./socketService.js";
3+
4+
/**
5+
* Persist a notification to MongoDB and push it in real-time to the user's
6+
* personal socket room via the `notification:new` event.
7+
*
8+
* Always fire-and-forget (never throws) — callers don't need try/catch.
9+
*/
10+
export async function pushNotification(userId, message, type = "course") {
11+
try {
12+
const notif = await Notification.create({ user: userId, message, type });
13+
emitToUser(userId.toString(), "notification:new", {
14+
id: notif._id,
15+
message: notif.message,
16+
type: notif.type,
17+
read: notif.read,
18+
createdAt: notif.createdAt,
19+
});
20+
} catch {
21+
// non-critical — never crash the main request
22+
}
23+
}

0 commit comments

Comments
 (0)