diff --git a/src/app/api/notifications/[id]/route.ts b/src/app/api/notifications/[id]/route.ts
new file mode 100644
index 00000000..c4eee600
--- /dev/null
+++ b/src/app/api/notifications/[id]/route.ts
@@ -0,0 +1,9 @@
+import { NotificationController } from '@/modules/notification/notification.controller';
+
+export async function DELETE(req: Request) {
+ return NotificationController.deleteNotification(req);
+}
+
+export async function PATCH(req: Request) {
+ return NotificationController.markAsRead(req);
+}
diff --git a/src/app/api/notifications/mark-all-read/route.ts b/src/app/api/notifications/mark-all-read/route.ts
new file mode 100644
index 00000000..7cffa78f
--- /dev/null
+++ b/src/app/api/notifications/mark-all-read/route.ts
@@ -0,0 +1,5 @@
+import { NotificationController } from '@/modules/notification/notification.controller';
+
+export async function POST(req: Request) {
+ return NotificationController.markAllAsRead(req);
+}
diff --git a/src/app/api/notifications/route.ts b/src/app/api/notifications/route.ts
new file mode 100644
index 00000000..f1d1178a
--- /dev/null
+++ b/src/app/api/notifications/route.ts
@@ -0,0 +1,13 @@
+import { NotificationController } from '@/modules/notification/notification.controller';
+
+export async function GET(req: Request) {
+ return NotificationController.getNotifications(req);
+}
+
+export async function POST(req: Request) {
+ return NotificationController.createNotification(req);
+}
+
+export async function DELETE(req: Request) {
+ return NotificationController.deleteAllRead(req);
+}
diff --git a/src/app/api/notifications/unread-count/route.ts b/src/app/api/notifications/unread-count/route.ts
new file mode 100644
index 00000000..4431a003
--- /dev/null
+++ b/src/app/api/notifications/unread-count/route.ts
@@ -0,0 +1,5 @@
+import { NotificationController } from '@/modules/notification/notification.controller';
+
+export async function GET(req: Request) {
+ return NotificationController.getUnreadCount(req);
+}
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
index 9a5c8787..85b936a5 100644
--- a/src/app/dashboard/page.tsx
+++ b/src/app/dashboard/page.tsx
@@ -10,6 +10,7 @@ import { DashboardStats } from './components/DashboardStats';
import { PerformanceChart } from './components/PerformanceChart';
import { ActivityFeed } from './components/ActivityFeed';
import ResumeCard from '@/components/ResumeCard';
+import NotificationCenter from '@/components/NotificationCenter';
interface UserProps {
name: string;
@@ -161,19 +162,7 @@ export default function DashboardPage() {
-
+
diff --git a/src/components/NotificationCenter.tsx b/src/components/NotificationCenter.tsx
new file mode 100644
index 00000000..6488e0ba
--- /dev/null
+++ b/src/components/NotificationCenter.tsx
@@ -0,0 +1,402 @@
+'use client';
+
+import { useState, useEffect, useRef, useCallback } from 'react';
+import authFetch from '@/lib/auth/authFetch';
+
+interface Notification {
+ id: string;
+ type: string;
+ title: string;
+ message: string;
+ isRead: boolean;
+ data?: Record
;
+ createdAt: string;
+ readAt?: string | null;
+}
+
+const notificationIcons: Record = {
+ QUIZ_REMINDER: (
+
+ ),
+ GOAL_COMPLETION: (
+
+ ),
+ STUDY_STREAK: (
+
+ ),
+ REVISION_RECOMMENDATION: (
+
+ ),
+ PERFORMANCE_INSIGHT: (
+
+ ),
+ WEEKLY_PROGRESS: (
+
+ ),
+ SYLLABUS_MILESTONE: (
+
+ ),
+ QUIZ_COMPLETION: (
+
+ ),
+ DAILY_STUDY_REMINDER: (
+
+ ),
+};
+
+const defaultIcon = (
+
+);
+
+function formatTimeAgo(dateString: string): string {
+ const date = new Date(dateString);
+ const now = new Date();
+ const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
+
+ if (diffInSeconds < 60) {
+ return 'Just now';
+ } else if (diffInSeconds < 3600) {
+ const minutes = Math.floor(diffInSeconds / 60);
+ return `${minutes}m ago`;
+ } else if (diffInSeconds < 86400) {
+ const hours = Math.floor(diffInSeconds / 3600);
+ return `${hours}h ago`;
+ } else if (diffInSeconds < 604800) {
+ const days = Math.floor(diffInSeconds / 86400);
+ return `${days}d ago`;
+ } else {
+ return date.toLocaleDateString();
+ }
+}
+
+export function NotificationCenter() {
+ const [isOpen, setIsOpen] = useState(false);
+ const [notifications, setNotifications] = useState([]);
+ const [unreadCount, setUnreadCount] = useState(0);
+ const [isLoading, setIsLoading] = useState(false);
+ const [activeTab, setActiveTab] = useState<'all' | 'unread'>('all');
+ const dropdownRef = useRef(null);
+
+ const fetchNotifications = useCallback(async () => {
+ setIsLoading(true);
+ try {
+ const res = await authFetch({
+ url: `/api/notifications?limit=20&offset=0&isRead=${activeTab === 'unread' ? 'false' : ''}`,
+ options: { method: 'GET' },
+ });
+
+ if (res?.data) {
+ setNotifications(res.data.notifications || []);
+ setUnreadCount(res.data.unreadCount || 0);
+ }
+ } catch (error) {
+ console.error('Failed to fetch notifications:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ }, [activeTab]);
+
+ const markAsRead = async (id: string) => {
+ try {
+ await authFetch({
+ url: `/api/notifications/${id}/read`,
+ options: { method: 'PATCH' },
+ });
+ setNotifications((prev) =>
+ prev.map((n) => (n.id === id ? { ...n, isRead: true } : n))
+ );
+ setUnreadCount((prev) => Math.max(0, prev - 1));
+ } catch (error) {
+ console.error('Failed to mark as read:', error);
+ }
+ };
+
+ const markAllAsRead = async () => {
+ try {
+ await authFetch({
+ url: '/api/notifications/mark-all-read',
+ options: { method: 'POST' },
+ });
+ setNotifications((prev) => prev.map((n) => ({ ...n, isRead: true })));
+ setUnreadCount(0);
+ } catch (error) {
+ console.error('Failed to mark all as read:', error);
+ }
+ };
+
+ const deleteNotification = async (id: string) => {
+ try {
+ await authFetch({
+ url: `/api/notifications/${id}`,
+ options: { method: 'DELETE' },
+ });
+ const deleted = notifications.find((n) => n.id === id);
+ setNotifications((prev) => prev.filter((n) => n.id !== id));
+ if (deleted && !deleted.isRead) {
+ setUnreadCount((prev) => Math.max(0, prev - 1));
+ }
+ } catch (error) {
+ console.error('Failed to delete notification:', error);
+ }
+ };
+
+ useEffect(() => {
+ fetchNotifications();
+ }, [fetchNotifications]);
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
+ setIsOpen(false);
+ }
+ };
+
+ if (isOpen) {
+ document.addEventListener('mousedown', handleClickOutside);
+ }
+
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [isOpen]);
+
+ useEffect(() => {
+ const fetchUnreadCount = async () => {
+ try {
+ const res = await authFetch({
+ url: '/api/notifications/unread-count',
+ options: { method: 'GET' },
+ });
+ if (res?.data?.unreadCount !== undefined) {
+ setUnreadCount(res.data.unreadCount);
+ }
+ } catch (error) {
+ console.error('Failed to fetch unread count:', error);
+ }
+ };
+
+ fetchUnreadCount();
+ const interval = setInterval(fetchUnreadCount, 60000);
+ return () => clearInterval(interval);
+ }, []);
+
+ const filteredNotifications = activeTab === 'unread'
+ ? notifications.filter((n) => !n.isRead)
+ : notifications;
+
+ return (
+
+ {/* Bell Icon Button */}
+
+
+ {/* Notification Dropdown */}
+ {isOpen && (
+
+ {/* Header */}
+
+
Notifications
+ {unreadCount > 0 && (
+
+ )}
+
+
+ {/* Tabs */}
+
+
+
+
+
+ {/* Notification List */}
+
+ {isLoading ? (
+
+ ) : filteredNotifications.length === 0 ? (
+
+
+
No notifications yet
+
+ ) : (
+
+ {filteredNotifications.map((notification) => (
+
{
+ if (!notification.isRead) {
+ markAsRead(notification.id);
+ }
+ }}
+ >
+ {/* Unread indicator */}
+ {!notification.isRead && (
+
+ )}
+
+
+ {/* Icon */}
+
+ {notificationIcons[notification.type] || defaultIcon}
+
+
+ {/* Content */}
+
+
+
+ {notification.title}
+
+
+
+
+ {notification.message}
+
+
+ {formatTimeAgo(notification.createdAt)}
+
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Footer */}
+
+
+
+
+ )}
+
+ );
+}
+
+export default NotificationCenter;
diff --git a/src/modules/notification/index.ts b/src/modules/notification/index.ts
new file mode 100644
index 00000000..ba3cae28
--- /dev/null
+++ b/src/modules/notification/index.ts
@@ -0,0 +1,4 @@
+export { NotificationController } from './notification.controller';
+export { NotificationService } from './notification.service';
+export { NotificationRepository } from './notification.repository';
+export * from './notification.types';
diff --git a/src/modules/notification/notification.controller.ts b/src/modules/notification/notification.controller.ts
new file mode 100644
index 00000000..6ad8324a
--- /dev/null
+++ b/src/modules/notification/notification.controller.ts
@@ -0,0 +1,165 @@
+import { NextResponse } from 'next/server';
+import { ZodError } from 'zod';
+import { withAuth, type AuthContext } from '@/lib/middleware/auth.middleware';
+import { NotificationService } from './notification.service';
+import { NotificationType } from '@/generated/prisma';
+
+const handleError = (error: unknown) => {
+ if (error instanceof ZodError) {
+ return NextResponse.json(
+ { success: false, message: 'Invalid request body', errors: error.issues },
+ { status: 400 }
+ );
+ }
+
+ const message = error instanceof Error ? error.message : 'Internal server error';
+
+ let status = 500;
+ if (message.includes('not found')) status = 404;
+ else if (message.includes('Unauthorized')) status = 403;
+
+ return NextResponse.json({ success: false, message }, { status });
+};
+
+function extractIdFromUrl(url: string): string | null {
+ const urlObj = new URL(url);
+ const pathSegments = urlObj.pathname.split('/').filter(Boolean);
+ // Expected path: /api/notifications/[id]
+ // pathSegments would be: ['api', 'notifications', '[id]'] or ['api', 'notifications', 'some-uuid']
+ const notificationsIndex = pathSegments.indexOf('notifications');
+ if (notificationsIndex !== -1 && pathSegments[notificationsIndex + 1]) {
+ const id = pathSegments[notificationsIndex + 1];
+ if (id !== 'mark-all-read' && id !== 'unread-count') {
+ return id;
+ }
+ }
+ return null;
+}
+
+export class NotificationController {
+ // GET /api/notifications - get all user's notifications
+ static getNotifications = withAuth(async (req: Request, auth: AuthContext) => {
+ try {
+ const { searchParams } = new URL(req.url);
+ const isRead = searchParams.get('isRead');
+ const type = searchParams.get('type') as NotificationType | null;
+ const limit = parseInt(searchParams.get('limit') || '20');
+ const offset = parseInt(searchParams.get('offset') || '0');
+
+ const result = await NotificationService.getNotifications({
+ userId: auth.userId,
+ isRead: isRead === 'true' ? true : isRead === 'false' ? false : undefined,
+ type: type || undefined,
+ limit,
+ offset,
+ });
+
+ return NextResponse.json({ success: true, data: result });
+ } catch (error) {
+ return handleError(error);
+ }
+ });
+
+ // GET /api/notifications/unread-count - get unread notification count
+ static getUnreadCount = withAuth(async (_req: Request, auth: AuthContext) => {
+ try {
+ const count = await NotificationService.getUnreadCount(auth.userId);
+ return NextResponse.json({ success: true, data: { unreadCount: count } });
+ } catch (error) {
+ return handleError(error);
+ }
+ });
+
+ // POST /api/notifications - create a notification (admin/system use)
+ static createNotification = withAuth(async (req: Request, auth: AuthContext) => {
+ try {
+ const body = await req.json();
+ const { userId, type, title, message, data } = body;
+
+ if (!userId || !type || !title || !message) {
+ return NextResponse.json(
+ { success: false, message: 'userId, type, title, and message are required' },
+ { status: 400 }
+ );
+ }
+
+ // Validate notification type
+ const validTypes = Object.values(NotificationType);
+ if (!validTypes.includes(type)) {
+ return NextResponse.json(
+ { success: false, message: `Invalid notification type. Must be one of: ${validTypes.join(', ')}` },
+ { status: 400 }
+ );
+ }
+
+ const notification = await NotificationService.createNotification({
+ userId,
+ type,
+ title,
+ message,
+ data,
+ });
+
+ return NextResponse.json(
+ { success: true, message: 'Notification created successfully', data: notification },
+ { status: 201 }
+ );
+ } catch (error) {
+ return handleError(error);
+ }
+ });
+
+ // PATCH /api/notifications/[id] - mark notification as read
+ static markAsRead = withAuth(async (req: Request, auth: AuthContext) => {
+ try {
+ const id = extractIdFromUrl(req.url);
+ if (!id) {
+ return NextResponse.json(
+ { success: false, message: 'Notification ID is required' },
+ { status: 400 }
+ );
+ }
+ const result = await NotificationService.markAsRead(id, auth.userId);
+ return NextResponse.json({ success: true, data: result });
+ } catch (error) {
+ return handleError(error);
+ }
+ });
+
+ // POST /api/notifications/mark-all-read - mark all notifications as read
+ static markAllAsRead = withAuth(async (_req: Request, auth: AuthContext) => {
+ try {
+ const result = await NotificationService.markAllAsRead(auth.userId);
+ return NextResponse.json({ success: true, data: result });
+ } catch (error) {
+ return handleError(error);
+ }
+ });
+
+ // DELETE /api/notifications/[id] - delete a notification
+ static deleteNotification = withAuth(async (req: Request, auth: AuthContext) => {
+ try {
+ const id = extractIdFromUrl(req.url);
+ if (!id) {
+ return NextResponse.json(
+ { success: false, message: 'Notification ID is required' },
+ { status: 400 }
+ );
+ }
+ const result = await NotificationService.deleteNotification(id, auth.userId);
+ return NextResponse.json({ success: true, data: result });
+ } catch (error) {
+ return handleError(error);
+ }
+ });
+
+ // DELETE /api/notifications - delete all read notifications
+ static deleteAllRead = withAuth(async (_req: Request, auth: AuthContext) => {
+ try {
+ const result = await NotificationService.deleteAllRead(auth.userId);
+ return NextResponse.json({ success: true, data: result });
+ } catch (error) {
+ return handleError(error);
+ }
+ });
+}
diff --git a/src/modules/notification/notification.repository.ts b/src/modules/notification/notification.repository.ts
new file mode 100644
index 00000000..ccd5a056
--- /dev/null
+++ b/src/modules/notification/notification.repository.ts
@@ -0,0 +1,95 @@
+import { prisma } from '@/lib/prisma';
+import type { NotificationFilters, CreateNotificationInput } from './notification.types';
+
+export class NotificationRepository {
+ static async findByUser(filters: NotificationFilters) {
+ const { userId, isRead, type, limit = 20, offset = 0 } = filters;
+
+ const where: Record = { userId };
+ if (isRead !== undefined) {
+ where.isRead = isRead;
+ }
+ if (type) {
+ where.type = type;
+ }
+
+ const [notifications, total, unreadCount] = await Promise.all([
+ prisma.notification.findMany({
+ where,
+ orderBy: { createdAt: 'desc' },
+ take: limit,
+ skip: offset,
+ }),
+ prisma.notification.count({ where }),
+ prisma.notification.count({ where: { userId, isRead: false } }),
+ ]);
+
+ return { notifications, total, unreadCount };
+ }
+
+ static async findById(id: string) {
+ return prisma.notification.findUnique({
+ where: { id },
+ });
+ }
+
+ static async create(input: CreateNotificationInput) {
+ return prisma.notification.create({
+ data: {
+ userId: input.userId,
+ type: input.type,
+ title: input.title,
+ message: input.message,
+ data: input.data,
+ },
+ });
+ }
+
+ static async markAsRead(id: string, userId: string) {
+ return prisma.notification.updateMany({
+ where: { id, userId },
+ data: {
+ isRead: true,
+ readAt: new Date(),
+ },
+ });
+ }
+
+ static async markAllAsRead(userId: string) {
+ return prisma.notification.updateMany({
+ where: { userId, isRead: false },
+ data: {
+ isRead: true,
+ readAt: new Date(),
+ },
+ });
+ }
+
+ static async delete(id: string, userId: string) {
+ const notification = await this.findById(id);
+
+ if (!notification) {
+ throw new Error('Notification not found');
+ }
+
+ if (notification.userId !== userId) {
+ throw new Error('Unauthorized to delete this notification');
+ }
+
+ return prisma.notification.delete({
+ where: { id },
+ });
+ }
+
+ static async deleteAllRead(userId: string) {
+ return prisma.notification.deleteMany({
+ where: { userId, isRead: true },
+ });
+ }
+
+ static async getUnreadCount(userId: string) {
+ return prisma.notification.count({
+ where: { userId, isRead: false },
+ });
+ }
+}
diff --git a/src/modules/notification/notification.service.ts b/src/modules/notification/notification.service.ts
new file mode 100644
index 00000000..93247c11
--- /dev/null
+++ b/src/modules/notification/notification.service.ts
@@ -0,0 +1,156 @@
+import { NotificationRepository } from './notification.repository';
+import type { NotificationFilters, CreateNotificationInput } from './notification.types';
+
+export class NotificationService {
+ static async getNotifications(filters: NotificationFilters) {
+ const result = await NotificationRepository.findByUser(filters);
+ return {
+ notifications: result.notifications,
+ unreadCount: result.unreadCount,
+ total: result.total,
+ };
+ }
+
+ static async getUnreadCount(userId: string) {
+ return NotificationRepository.getUnreadCount(userId);
+ }
+
+ static async createNotification(input: CreateNotificationInput) {
+ return NotificationRepository.create(input);
+ }
+
+ static async markAsRead(id: string, userId: string) {
+ const notification = await NotificationRepository.findById(id);
+
+ if (!notification) {
+ throw new Error('Notification not found');
+ }
+
+ if (notification.userId !== userId) {
+ throw new Error('Unauthorized to mark this notification as read');
+ }
+
+ return NotificationRepository.markAsRead(id, userId);
+ }
+
+ static async markAllAsRead(userId: string) {
+ return NotificationRepository.markAllAsRead(userId);
+ }
+
+ static async deleteNotification(id: string, userId: string) {
+ return NotificationRepository.delete(id, userId);
+ }
+
+ static async deleteAllRead(userId: string) {
+ return NotificationRepository.deleteAllRead(userId);
+ }
+
+ // Helper methods for creating specific notification types
+ static async createQuizReminder(userId: string, quizTitle: string, dueDate?: Date) {
+ const message = dueDate
+ ? `Your quiz "${quizTitle}" is due on ${dueDate.toLocaleDateString()}. Don't forget to complete it!`
+ : `Time to practice! Take a quiz on "${quizTitle}" to strengthen your understanding.`;
+
+ return this.createNotification({
+ userId,
+ type: 'QUIZ_REMINDER',
+ title: 'Quiz Reminder',
+ message,
+ data: { quizTitle, dueDate },
+ });
+ }
+
+ static async createGoalCompletion(userId: string, goalTitle: string, achievement: string) {
+ return this.createNotification({
+ userId,
+ type: 'GOAL_COMPLETION',
+ title: 'Goal Achieved! 🎉',
+ message: `Congratulations! You've completed your goal: "${goalTitle}". ${achievement}`,
+ data: { goalTitle, achievement },
+ });
+ }
+
+ static async createStreakAlert(userId: string, currentStreak: number) {
+ const streakMessages: Record = {
+ 7: "A whole week! Your dedication is inspiring.",
+ 14: "Two weeks strong! You're building excellent habits.",
+ 30: "A month of consistent learning! You're unstoppable.",
+ 60: "Two months! You've mastered the art of consistency.",
+ 100: "100 days! You're a learning champion!",
+ };
+
+ const message = streakMessages[currentStreak] || `Keep it up! You're on a ${currentStreak}-day streak!`;
+
+ return this.createNotification({
+ userId,
+ type: 'STUDY_STREAK',
+ title: 'Study Streak Milestone',
+ message,
+ data: { streak: currentStreak },
+ });
+ }
+
+ static async createRevisionRecommendation(userId: string, topic: string, reason: string) {
+ return this.createNotification({
+ userId,
+ type: 'REVISION_RECOMMENDATION',
+ title: 'Time to Review',
+ message: `We noticed you might benefit from reviewing "${topic}". ${reason}`,
+ data: { topic, reason },
+ });
+ }
+
+ static async createPerformanceInsight(userId: string, insight: string, improvement: string) {
+ return this.createNotification({
+ userId,
+ type: 'PERFORMANCE_INSIGHT',
+ title: 'Performance Insight',
+ message: `${insight}. Here's a tip: ${improvement}`,
+ });
+ }
+
+ static async createWeeklyProgress(userId: string, hoursStudied: number, accuracy: number) {
+ return this.createNotification({
+ userId,
+ type: 'WEEKLY_PROGRESS',
+ title: 'Weekly Progress Report',
+ message: `This week: ${hoursStudied} hours of study time with ${accuracy}% accuracy. Keep up the great work!`,
+ data: { hoursStudied, accuracy },
+ });
+ }
+
+ static async createSyllabusMilestone(userId: string, chapterName: string, progress: number) {
+ return this.createNotification({
+ userId,
+ type: 'SYLLABUS_MILESTONE',
+ title: 'Chapter Complete! 📚',
+ message: `You've completed ${progress}% of "${chapterName}". Keep pushing forward!`,
+ data: { chapterName, progress },
+ });
+ }
+
+ static async createQuizCompletion(userId: string, quizTitle: string, score: number, accuracy: number) {
+ const emoji = accuracy >= 80 ? '🌟' : accuracy >= 60 ? '👍' : '💪';
+ return this.createNotification({
+ userId,
+ type: 'QUIZ_COMPLETION',
+ title: `Quiz Complete ${emoji}`,
+ message: `You scored ${score}/${accuracy}% on "${quizTitle}". ${accuracy >= 80 ? 'Excellent work!' : 'Keep practicing!'}`,
+ data: { quizTitle, score, accuracy },
+ });
+ }
+
+ static async createDailyStudyReminder(userId: string, suggestedTopic?: string) {
+ const message = suggestedTopic
+ ? `Start your day strong! We suggest studying "${suggestedTopic}" today.`
+ : "Good morning! A new day of learning awaits. Let's build your streak!";
+
+ return this.createNotification({
+ userId,
+ type: 'DAILY_STUDY_REMINDER',
+ title: 'Daily Study Reminder',
+ message,
+ data: { suggestedTopic },
+ });
+ }
+}
diff --git a/src/modules/notification/notification.types.ts b/src/modules/notification/notification.types.ts
new file mode 100644
index 00000000..45e5f23d
--- /dev/null
+++ b/src/modules/notification/notification.types.ts
@@ -0,0 +1,35 @@
+import { NotificationType } from '@/generated/prisma';
+
+export interface Notification {
+ id: string;
+ userId: string;
+ type: NotificationType;
+ title: string;
+ message: string;
+ isRead: boolean;
+ data?: Record;
+ createdAt: Date;
+ readAt?: Date | null;
+}
+
+export interface CreateNotificationInput {
+ userId: string;
+ type: NotificationType;
+ title: string;
+ message: string;
+ data?: Record;
+}
+
+export interface NotificationFilters {
+ userId: string;
+ isRead?: boolean;
+ type?: NotificationType;
+ limit?: number;
+ offset?: number;
+}
+
+export interface NotificationResponse {
+ success: boolean;
+ message?: string;
+ data?: Notification | Notification[] | { notifications: Notification[]; unreadCount: number; total: number };
+}
diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma
index 040ecc6f..de5debbc 100644
--- a/src/prisma/schema.prisma
+++ b/src/prisma/schema.prisma
@@ -31,6 +31,7 @@ model User {
refreshTokens RefreshToken[]
stats UserStats?
bookmarks Bookmark[]
+ notifications Notification[]
@@index([email])
}
@@ -261,6 +262,34 @@ model Bookmark {
@@index([chapterId])
}
+model Notification {
+ id String @id @default(uuid())
+ userId String
+ type NotificationType
+ title String
+ message String
+ isRead Boolean @default(false)
+ data Json?
+ createdAt DateTime @default(now())
+ readAt DateTime?
+
+ @@index([userId])
+ @@index([userId, isRead])
+ @@index([createdAt])
+}
+
+enum NotificationType {
+ QUIZ_REMINDER
+ GOAL_COMPLETION
+ STUDY_STREAK
+ REVISION_RECOMMENDATION
+ PERFORMANCE_INSIGHT
+ WEEKLY_PROGRESS
+ SYLLABUS_MILESTONE
+ QUIZ_COMPLETION
+ DAILY_STUDY_REMINDER
+}
+
enum UserRole {
STUDENT