diff --git a/client/src/components/layout/Navbar.tsx b/client/src/components/layout/Navbar.tsx index 4a44c53..5b5d995 100644 --- a/client/src/components/layout/Navbar.tsx +++ b/client/src/components/layout/Navbar.tsx @@ -20,6 +20,7 @@ import AdminNavLink from "../admin/AdminNavLink"; import { getUserProfile, logout } from "../../utils/api"; import type { User as UserType } from "../../types"; import { removeAuthToken } from "../../utils/auth"; +import NotificationDropdown from "../ui/NotificationDropdown"; interface NavbarProps { authenticated: boolean | null; @@ -361,6 +362,9 @@ export default function ResponsiveNavbar({ )} + {/* Notifications */} + {authenticated && } + // {/* Auth area (preserved) */} {authenticated ? (
diff --git a/client/src/components/ui/NotificationDropdown.tsx b/client/src/components/ui/NotificationDropdown.tsx index 1cc1df7..a42254b 100644 --- a/client/src/components/ui/NotificationDropdown.tsx +++ b/client/src/components/ui/NotificationDropdown.tsx @@ -15,14 +15,179 @@ import { markNotificationAsRead, markAllNotificationsAsRead, } from "../../utils/api"; -import { Notification } from "../../types/discussions"; -import useSocket from "../../hooks/useSocket"; +import { Notification as NotificationType } from "../../types/discussions"; +import { useSocket } from "../../contexts/SocketContext"; + +// ๐Ÿงช MOCK DATA - Only for testing without backend +/* +const allMockNotifications: NotificationType[] = [ + { + id: 1, + user_id: 1, + type: "new_answer", + title: "New Answer on Your Question", + message: "John Doe answered your question about React hooks", + related_id: 123, + related_type: "discussion", + from_user_id: 2, + from_username: "johndoe", + is_read: 0, + created_at: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + }, + { + id: 2, + user_id: 1, + type: "mention", + title: "You were mentioned", + message: "Alice mentioned you in a discussion about TypeScript", + related_id: 456, + related_type: "discussion", + from_user_id: 3, + from_username: "alice_dev", + is_read: 0, + created_at: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), + }, + { + id: 3, + user_id: 1, + type: "best_answer", + title: "Best Answer Selected! ๐ŸŽ‰", + message: "Your answer was marked as the best answer", + related_id: 789, + related_type: "discussion", + from_user_id: 4, + from_username: "mike_smith", + is_read: 1, + created_at: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + }, + { + id: 4, + user_id: 1, + type: "reply", + title: "New Reply", + message: "Sarah replied to your answer about JavaScript closures", + related_id: 321, + related_type: "discussion", + from_user_id: 5, + from_username: "sarah_codes", + is_read: 1, + created_at: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), + }, + { + id: 5, + user_id: 1, + type: "new_answer", + title: "Multiple New Answers", + message: "3 people answered your question about Node.js streams", + related_id: 654, + related_type: "discussion", + from_user_id: 6, + from_username: "dev_expert", + is_read: 0, + created_at: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), + }, + { + id: 6, + user_id: 1, + type: "mention", + title: "Tagged in Discussion", + message: "David tagged you in a discussion about Python async/await", + related_id: 987, + related_type: "discussion", + from_user_id: 7, + from_username: "david_py", + is_read: 0, + created_at: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000).toISOString(), + }, + { + id: 7, + user_id: 1, + type: "reply", + title: "New Reply on Discussion", + message: "Emma replied to your comment about database indexing", + related_id: 234, + related_type: "discussion", + from_user_id: 8, + from_username: "emma_db", + is_read: 1, + created_at: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + }, + { + id: 8, + user_id: 1, + type: "new_answer", + title: "Question Answered", + message: "Chris answered your question about Docker containers", + related_id: 567, + related_type: "discussion", + from_user_id: 9, + from_username: "chris_devops", + is_read: 1, + created_at: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(), + }, + { + id: 9, + user_id: 1, + type: "best_answer", + title: "Your Answer Was Chosen!", + message: "Your answer about Git workflows was marked as best", + related_id: 890, + related_type: "discussion", + from_user_id: 10, + from_username: "lisa_git", + is_read: 1, + created_at: new Date(Date.now() - 12 * 24 * 60 * 60 * 1000).toISOString(), + }, + { + id: 10, + user_id: 1, + type: "reply", + title: "Discussion Activity", + message: "Tom replied to your answer about REST APIs", + related_id: 345, + related_type: "discussion", + from_user_id: 11, + from_username: "tom_api", + is_read: 1, + created_at: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString(), + }, + { + id: 11, + user_id: 1, + type: "mention", + title: "You were mentioned", + message: "Nina mentioned you in a discussion about MongoDB", + related_id: 678, + related_type: "discussion", + from_user_id: 12, + from_username: "nina_mongo", + is_read: 1, + created_at: new Date(Date.now() - 20 * 24 * 60 * 60 * 1000).toISOString(), + }, + { + id: 12, + user_id: 1, + type: "new_answer", + title: "New Answer Posted", + message: "Kevin answered your question about GraphQL", + related_id: 901, + related_type: "discussion", + from_user_id: 13, + from_username: "kevin_graphql", + is_read: 1, + created_at: new Date(Date.now() - 25 * 24 * 60 * 60 * 1000).toISOString(), + }, +]; +*/ const NotificationDropdown: React.FC = () => { - const [notifications, setNotifications] = useState([]); + const [notifications, setNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0); const [isOpen, setIsOpen] = useState(false); const [loading, setLoading] = useState(false); + const [loadingMore, setLoadingMore] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const [hasMore, setHasMore] = useState(true); // Set to true to show Load More button const dropdownRef = useRef(null); const navigate = useNavigate(); const socket = useSocket(); @@ -39,7 +204,7 @@ const NotificationDropdown: React.FC = () => { useEffect(() => { if (!socket) return; - socket.on("new_notification", (notification: Notification) => { + socket.on("new_notification", (notification: NotificationType) => { setNotifications((prev) => [notification, ...prev]); setUnreadCount((prev) => prev + 1); @@ -72,12 +237,45 @@ const NotificationDropdown: React.FC = () => { }; }, []); - const fetchNotifications = async () => { + const fetchNotifications = async (page = 1, append = false) => { try { - setLoading(true); - const response = await getNotifications(); - setNotifications(response.notifications); + if (append) { + setLoadingMore(true); + } else { + setLoading(true); + } + + // Real API call + const response = await getNotifications(page, 5); // 5 notifications per page + + if (append) { + setNotifications((prev) => [...prev, ...response.notifications]); + } else { + setNotifications(response.notifications); + } + setUnreadCount(response.unreadCount); + setCurrentPage(page); + setHasMore(page < response.pagination.pages); + + /* ๐Ÿงช MOCK DATA LOGIC - Only for testing without backend + const LIMIT = 5; + const skip = (page - 1) * LIMIT; + const mockPage = allMockNotifications.slice(skip, skip + LIMIT); + const totalPages = Math.ceil(allMockNotifications.length / LIMIT); + + await new Promise(resolve => setTimeout(resolve, 500)); + + if (append) { + setNotifications((prev) => [...prev, ...mockPage]); + } else { + setNotifications(mockPage); + } + + setUnreadCount(allMockNotifications.filter(n => n.is_read === 0).length); + setCurrentPage(page); + setHasMore(page < totalPages); + */ // Request notification permission if not already granted if (Notification.permission === "default") { @@ -88,10 +286,11 @@ const NotificationDropdown: React.FC = () => { // Don't show error to user for notifications, just log it } finally { setLoading(false); + setLoadingMore(false); } }; - const handleNotificationClick = async (notification: Notification) => { + const handleNotificationClick = async (notification: NotificationType) => { try { if (!notification.is_read) { await markNotificationAsRead(notification.id.toString()); @@ -127,6 +326,12 @@ const NotificationDropdown: React.FC = () => { } }; + const handleLoadMore = async () => { + if (!loadingMore && hasMore) { + await fetchNotifications(currentPage + 1, true); // Load next page and append + } + }; + const getNotificationIcon = (type: string) => { switch (type) { case "new_answer": @@ -249,6 +454,19 @@ const NotificationDropdown: React.FC = () => {
)) )} + + {/* Load More Button */} + {hasMore && notifications.length > 0 && ( +
+ +
+ )} {/* Footer */} diff --git a/client/src/utils/api.ts b/client/src/utils/api.ts index c87720e..57f2f9d 100644 --- a/client/src/utils/api.ts +++ b/client/src/utils/api.ts @@ -474,11 +474,19 @@ export const voteReply = async ( return response.data; }; -export const getNotifications = async (): Promise<{ +export const getNotifications = async (page = 1, limit = 20): Promise<{ notifications: Notification[]; unreadCount: number; + pagination: { + page: number; + limit: number; + total: number; + pages: number; + }; }> => { - const response = await api.get("/notifications"); + const response = await api.get("/notifications", { + params: { page, limit }, + }); return response.data; };