From fee3f74acf2c326e594cde48fc20f896c59c2e75 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 04:19:05 +0000 Subject: [PATCH 1/4] Implement notification center with milestones and premiere alerts - Added `notifications` collection and API routes (GET, PATCH, DELETE). - Implemented `milestoneService` to track watch time and trigger alerts every 100 hours. - Implemented `cronService` with `node-cron` to check for daily TV premieres. - Added `useNotificationStore` (Zustand) and real-time sync with Socket.IO. - Updated Frontend UI with a Notification Center modal and unread badge. - Verified with unit tests and Playwright. --- cinetrack-app/client/src/App.tsx | 2 + .../components/common/NotificationsModal.tsx | 132 +++++++++++++---- .../src/components/layout/SideNavBar.tsx | 10 +- .../client/src/layouts/RootLayout.tsx | 15 +- .../client/src/services/dbService.ts | 34 +++++ .../client/src/services/socketService.ts | 14 ++ .../client/src/store/useNotificationStore.ts | 131 +++++++++++++++++ cinetrack-app/package-lock.json | 10 ++ cinetrack-app/server/package.json | 1 + .../server/src/routes/notificationRoutes.js | 90 ++++++++++++ .../server/src/routes/watchlistRoutes.js | 26 +++- cinetrack-app/server/src/server.js | 16 +- .../server/src/services/cronService.js | 137 ++++++++++++++++++ .../server/src/services/milestoneService.js | 95 ++++++++++++ cinetrack-app/verification/notifications.png | Bin 0 -> 59639 bytes 15 files changed, 681 insertions(+), 32 deletions(-) create mode 100644 cinetrack-app/client/src/store/useNotificationStore.ts create mode 100644 cinetrack-app/server/src/routes/notificationRoutes.js create mode 100644 cinetrack-app/server/src/services/cronService.js create mode 100644 cinetrack-app/server/src/services/milestoneService.js create mode 100644 cinetrack-app/verification/notifications.png diff --git a/cinetrack-app/client/src/App.tsx b/cinetrack-app/client/src/App.tsx index 3dc8638..a0a809f 100644 --- a/cinetrack-app/client/src/App.tsx +++ b/cinetrack-app/client/src/App.tsx @@ -8,11 +8,13 @@ import { useDemoWelcome } from "./hooks/useDemoWelcome"; import { RouterProvider } from "react-router-dom"; import { router } from "./router"; import { useWatchlistInit } from "./store/useWatchlistStore"; +import { useNotificationInit } from "./store/useNotificationStore"; const AuthPage = lazy(() => import("./pages/AuthPage").then((m) => ({ default: m.AuthPage }))); const WatchlistInitializer: React.FC<{ children: React.ReactNode }> = ({ children }) => { useWatchlistInit(); + useNotificationInit(); return <>{children}; }; diff --git a/cinetrack-app/client/src/components/common/NotificationsModal.tsx b/cinetrack-app/client/src/components/common/NotificationsModal.tsx index 5c6d601..3580ac0 100644 --- a/cinetrack-app/client/src/components/common/NotificationsModal.tsx +++ b/cinetrack-app/client/src/components/common/NotificationsModal.tsx @@ -1,12 +1,38 @@ import React from "react"; -import { FiBell, FiX } from "react-icons/fi"; +import { FiBell, FiX, FiCheck, FiTrash2, FiInfo, FiAward, FiTv } from "react-icons/fi"; +import { useNotificationStore } from "../../store/useNotificationStore"; +import type { Notification } from "../../services/dbService"; interface NotificationsModalProps { isOpen: boolean; onClose: () => void; } +const timeAgo = (dateStr: string) => { + const date = new Date(dateStr); + const now = new Date(); + const seconds = Math.floor((now.getTime() - date.getTime()) / 1000); + + if (seconds < 60) return 'Just now'; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +}; + +const NotificationIcon = ({ type }: { type: Notification['type'] }) => { + switch (type) { + case 'milestone': return ; + case 'premiere': return ; + default: return ; + } +}; + export const NotificationsModal: React.FC = ({ isOpen, onClose }) => { + const { notifications, markAsRead, markAllAsRead, removeNotification } = useNotificationStore(); + if (!isOpen) return null; return ( @@ -17,39 +43,95 @@ export const NotificationsModal: React.FC = ({ isOpen, role="dialog" >
e.stopPropagation()} > -
+ {/* Header */} +

Notifications

- -
- -
-
- +
+ {notifications.some(n => !n.isRead) && ( + + )} +
-

Coming Soon

-

- Notifications are not available yet. Work in progress!! -

- + {/* List */} +
+ {notifications.length === 0 ? ( +
+
+ +
+

No notifications yet

+
+ ) : ( + notifications.map((n) => ( +
+
+
+ +
+
+
+

+ {n.title} +

+ + {timeAgo(n.createdAt)} + +
+

+ {n.message} +

+
+
+ + {/* Actions */} +
+ {!n.isRead && ( + + )} + +
+
+ )) + )} +
); diff --git a/cinetrack-app/client/src/components/layout/SideNavBar.tsx b/cinetrack-app/client/src/components/layout/SideNavBar.tsx index e59abf6..3a279bc 100644 --- a/cinetrack-app/client/src/components/layout/SideNavBar.tsx +++ b/cinetrack-app/client/src/components/layout/SideNavBar.tsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; import { useAuthContext } from "../../contexts/AuthContext"; +import { useNotificationStore } from "../../store/useNotificationStore"; import { ConfirmModal } from "../common/ConfirmModal"; import { FiCompass, @@ -63,6 +64,7 @@ export const SideNavBar: React.FC = ({ onOpenNotifications, }) => { const { user, logout } = useAuthContext(); + const { unreadCount } = useNotificationStore(); const [showLogoutConfirm, setShowLogoutConfirm] = useState(false); const [isHovered, setIsHovered] = useState(false); @@ -156,9 +158,11 @@ export const SideNavBar: React.FC = ({ > {isExpanded && Notifications} - + {unreadCount > 0 && ( + + )} { } }; + const unreadCount = useNotificationStore((state) => state.unreadCount); + return (
@@ -96,7 +99,11 @@ const Header: React.FC = memo(() => { aria-label="Notifications" > - + {unreadCount > 0 && ( + + + + )} )}