feat: Implement notification API service, integrate unread count in header, and replace mock data on notification pages.
This commit is contained in:
@@ -1,68 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { notificationsApiService, type AppNotification } from '@/services/notifications-api.service';
|
||||||
|
|
||||||
// Mock notification data - replace with actual API call when backend is ready
|
function getNotificationIcon(type: AppNotification['type']) {
|
||||||
interface Notification {
|
|
||||||
id: string;
|
|
||||||
type: 'connection' | 'message' | 'system' | 'update' | 'request';
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
timestamp: string;
|
|
||||||
read: boolean;
|
|
||||||
actionUrl?: string;
|
|
||||||
avatar?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockNotifications: Notification[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
type: 'request',
|
|
||||||
title: 'New Connection Request',
|
|
||||||
description: 'Jane Doe wants to connect with you',
|
|
||||||
timestamp: '1 hour ago',
|
|
||||||
read: false,
|
|
||||||
actionUrl: '/agent/requests',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
type: 'message',
|
|
||||||
title: 'New Message',
|
|
||||||
description: 'You have a new message from Mike Wilson',
|
|
||||||
timestamp: '3 hours ago',
|
|
||||||
read: false,
|
|
||||||
actionUrl: '/agent/message',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
type: 'connection',
|
|
||||||
title: 'Connection Accepted',
|
|
||||||
description: 'Your connection with Sarah Johnson has been confirmed',
|
|
||||||
timestamp: '1 day ago',
|
|
||||||
read: true,
|
|
||||||
actionUrl: '/agent/connections',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
type: 'system',
|
|
||||||
title: 'Profile Verification',
|
|
||||||
description: 'Your profile has been verified successfully',
|
|
||||||
timestamp: '2 days ago',
|
|
||||||
read: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
type: 'update',
|
|
||||||
title: 'New Features Available',
|
|
||||||
description: 'Check out the latest updates to your dashboard',
|
|
||||||
timestamp: '5 days ago',
|
|
||||||
read: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function getNotificationIcon(type: Notification['type']) {
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'connection':
|
case 'connection':
|
||||||
return '/assets/icons/user-placeholder-icon.svg';
|
return '/assets/icons/user-placeholder-icon.svg';
|
||||||
@@ -79,26 +22,69 @@ function getNotificationIcon(type: Notification['type']) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatTimestamp(dateStr: string): string {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = now.getTime() - date.getTime();
|
||||||
|
const diffMins = Math.floor(diffMs / 60000);
|
||||||
|
const diffHours = Math.floor(diffMs / 3600000);
|
||||||
|
const diffDays = Math.floor(diffMs / 86400000);
|
||||||
|
|
||||||
|
if (diffMins < 1) return 'Just now';
|
||||||
|
if (diffMins < 60) return `${diffMins} min ago`;
|
||||||
|
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
|
||||||
|
if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
|
||||||
|
return date.toLocaleDateString();
|
||||||
|
}
|
||||||
|
|
||||||
export default function AgentNotificationsPage() {
|
export default function AgentNotificationsPage() {
|
||||||
const [notifications, setNotifications] = useState<Notification[]>(mockNotifications);
|
const [notifications, setNotifications] = useState<AppNotification[]>([]);
|
||||||
|
const [unreadCount, setUnreadCount] = useState(0);
|
||||||
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
const [filter, setFilter] = useState<'all' | 'unread'>('all');
|
const [filter, setFilter] = useState<'all' | 'unread'>('all');
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
const filteredNotifications = filter === 'all'
|
const fetchNotifications = useCallback(async () => {
|
||||||
? notifications
|
try {
|
||||||
: notifications.filter(n => !n.read);
|
const data = await notificationsApiService.getNotifications(filter, 1, 50);
|
||||||
|
setNotifications(data.notifications);
|
||||||
|
setUnreadCount(data.unreadCount);
|
||||||
|
setTotalCount(data.pagination.total);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch notifications:', err);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [filter]);
|
||||||
|
|
||||||
const unreadCount = notifications.filter(n => !n.read).length;
|
useEffect(() => {
|
||||||
|
fetchNotifications();
|
||||||
|
}, [fetchNotifications]);
|
||||||
|
|
||||||
const markAsRead = (id: string) => {
|
const markAsRead = async (id: string) => {
|
||||||
setNotifications(prev =>
|
try {
|
||||||
prev.map(n => n.id === id ? { ...n, read: true } : n)
|
await notificationsApiService.markAsRead(id);
|
||||||
);
|
setNotifications((prev) =>
|
||||||
|
prev.map((n) => (n.id === id ? { ...n, read: true } : n))
|
||||||
|
);
|
||||||
|
setUnreadCount((prev) => Math.max(0, prev - 1));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to mark as read:', err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const markAllAsRead = () => {
|
const markAllAsRead = async () => {
|
||||||
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
|
try {
|
||||||
|
await notificationsApiService.markAllAsRead();
|
||||||
|
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
|
||||||
|
setUnreadCount(0);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to mark all as read:', err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const displayCount = filter === 'all' ? totalCount : unreadCount;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-7xl mx-auto px-4 lg:px-8 py-6">
|
<div className="max-w-7xl mx-auto px-4 lg:px-8 py-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -118,7 +104,9 @@ export default function AgentNotificationsPage() {
|
|||||||
Notifications
|
Notifications
|
||||||
</h1>
|
</h1>
|
||||||
<p className="font-serif text-[14px] text-[#00293D]/60">
|
<p className="font-serif text-[14px] text-[#00293D]/60">
|
||||||
{unreadCount > 0 ? `You have ${unreadCount} unread notification${unreadCount > 1 ? 's' : ''}` : 'All caught up!'}
|
{unreadCount > 0
|
||||||
|
? `You have ${unreadCount} unread notification${unreadCount > 1 ? 's' : ''}`
|
||||||
|
: 'All caught up!'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -143,7 +131,7 @@ export default function AgentNotificationsPage() {
|
|||||||
: 'bg-[#00293D]/10 text-[#00293D] hover:bg-[#00293D]/20'
|
: 'bg-[#00293D]/10 text-[#00293D] hover:bg-[#00293D]/20'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
All ({notifications.length})
|
All ({totalCount})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setFilter('unread')}
|
onClick={() => setFilter('unread')}
|
||||||
@@ -160,7 +148,11 @@ export default function AgentNotificationsPage() {
|
|||||||
|
|
||||||
{/* Notifications List */}
|
{/* Notifications List */}
|
||||||
<div className="border border-[#00293d]/10 rounded-[20px] bg-white overflow-hidden">
|
<div className="border border-[#00293d]/10 rounded-[20px] bg-white overflow-hidden">
|
||||||
{filteredNotifications.length === 0 ? (
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#e58625]"></div>
|
||||||
|
</div>
|
||||||
|
) : notifications.length === 0 ? (
|
||||||
<div className="p-12 text-center">
|
<div className="p-12 text-center">
|
||||||
<div className="w-16 h-16 rounded-full bg-[#00293D]/5 flex items-center justify-center mx-auto mb-4">
|
<div className="w-16 h-16 rounded-full bg-[#00293D]/5 flex items-center justify-center mx-auto mb-4">
|
||||||
<Image
|
<Image
|
||||||
@@ -177,7 +169,7 @@ export default function AgentNotificationsPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-[#00293d]/10">
|
<div className="divide-y divide-[#00293d]/10">
|
||||||
{filteredNotifications.map((notification) => (
|
{notifications.map((notification) => (
|
||||||
<div
|
<div
|
||||||
key={notification.id}
|
key={notification.id}
|
||||||
className={`p-4 hover:bg-[#00293D]/5 transition-colors ${
|
className={`p-4 hover:bg-[#00293D]/5 transition-colors ${
|
||||||
@@ -186,9 +178,11 @@ export default function AgentNotificationsPage() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
{/* Icon */}
|
{/* Icon */}
|
||||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
|
<div
|
||||||
!notification.read ? 'bg-[#e58625]/20' : 'bg-[#00293D]/10'
|
className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
|
||||||
}`}>
|
!notification.read ? 'bg-[#e58625]/20' : 'bg-[#00293D]/10'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
src={getNotificationIcon(notification.type)}
|
src={getNotificationIcon(notification.type)}
|
||||||
alt={notification.type}
|
alt={notification.type}
|
||||||
@@ -201,9 +195,13 @@ export default function AgentNotificationsPage() {
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className={`font-fractul text-[14px] leading-[17px] ${
|
<p
|
||||||
!notification.read ? 'font-bold text-[#00293D]' : 'font-medium text-[#00293D]/80'
|
className={`font-fractul text-[14px] leading-[17px] ${
|
||||||
}`}>
|
!notification.read
|
||||||
|
? 'font-bold text-[#00293D]'
|
||||||
|
: 'font-medium text-[#00293D]/80'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{notification.title}
|
{notification.title}
|
||||||
</p>
|
</p>
|
||||||
<p className="font-serif text-[14px] text-[#00293D]/60 mt-1">
|
<p className="font-serif text-[14px] text-[#00293D]/60 mt-1">
|
||||||
@@ -211,7 +209,7 @@ export default function AgentNotificationsPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-serif text-[12px] text-[#00293D]/50 flex-shrink-0">
|
<span className="font-serif text-[12px] text-[#00293D]/50 flex-shrink-0">
|
||||||
{notification.timestamp}
|
{formatTimestamp(notification.createdAt)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,60 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { notificationsApiService, type AppNotification } from '@/services/notifications-api.service';
|
||||||
|
|
||||||
// Mock notification data - replace with actual API call when backend is ready
|
function getNotificationIcon(type: AppNotification['type']) {
|
||||||
interface Notification {
|
|
||||||
id: string;
|
|
||||||
type: 'connection' | 'message' | 'system' | 'update';
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
timestamp: string;
|
|
||||||
read: boolean;
|
|
||||||
actionUrl?: string;
|
|
||||||
avatar?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockNotifications: Notification[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
type: 'connection',
|
|
||||||
title: 'New Connection Request',
|
|
||||||
description: 'John Smith wants to connect with you',
|
|
||||||
timestamp: '2 hours ago',
|
|
||||||
read: false,
|
|
||||||
actionUrl: '/user/connections',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
type: 'message',
|
|
||||||
title: 'New Message',
|
|
||||||
description: 'You have a new message from Sarah Johnson',
|
|
||||||
timestamp: '5 hours ago',
|
|
||||||
read: false,
|
|
||||||
actionUrl: '/user/message',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
type: 'system',
|
|
||||||
title: 'Profile Update Reminder',
|
|
||||||
description: 'Complete your profile to get better recommendations',
|
|
||||||
timestamp: '1 day ago',
|
|
||||||
read: true,
|
|
||||||
actionUrl: '/user/settings',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
type: 'update',
|
|
||||||
title: 'New Features Available',
|
|
||||||
description: 'Check out the latest updates to our platform',
|
|
||||||
timestamp: '3 days ago',
|
|
||||||
read: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function getNotificationIcon(type: Notification['type']) {
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'connection':
|
case 'connection':
|
||||||
return '/assets/icons/user-placeholder-icon.svg';
|
return '/assets/icons/user-placeholder-icon.svg';
|
||||||
@@ -69,24 +20,65 @@ function getNotificationIcon(type: Notification['type']) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatTimestamp(dateStr: string): string {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = now.getTime() - date.getTime();
|
||||||
|
const diffMins = Math.floor(diffMs / 60000);
|
||||||
|
const diffHours = Math.floor(diffMs / 3600000);
|
||||||
|
const diffDays = Math.floor(diffMs / 86400000);
|
||||||
|
|
||||||
|
if (diffMins < 1) return 'Just now';
|
||||||
|
if (diffMins < 60) return `${diffMins} min ago`;
|
||||||
|
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
|
||||||
|
if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
|
||||||
|
return date.toLocaleDateString();
|
||||||
|
}
|
||||||
|
|
||||||
export default function UserNotificationsPage() {
|
export default function UserNotificationsPage() {
|
||||||
const [notifications, setNotifications] = useState<Notification[]>(mockNotifications);
|
const [notifications, setNotifications] = useState<AppNotification[]>([]);
|
||||||
|
const [unreadCount, setUnreadCount] = useState(0);
|
||||||
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
const [filter, setFilter] = useState<'all' | 'unread'>('all');
|
const [filter, setFilter] = useState<'all' | 'unread'>('all');
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
const filteredNotifications = filter === 'all'
|
const fetchNotifications = useCallback(async () => {
|
||||||
? notifications
|
try {
|
||||||
: notifications.filter(n => !n.read);
|
const data = await notificationsApiService.getNotifications(filter, 1, 50);
|
||||||
|
setNotifications(data.notifications);
|
||||||
|
setUnreadCount(data.unreadCount);
|
||||||
|
setTotalCount(data.pagination.total);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch notifications:', err);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [filter]);
|
||||||
|
|
||||||
const unreadCount = notifications.filter(n => !n.read).length;
|
useEffect(() => {
|
||||||
|
fetchNotifications();
|
||||||
|
}, [fetchNotifications]);
|
||||||
|
|
||||||
const markAsRead = (id: string) => {
|
const markAsRead = async (id: string) => {
|
||||||
setNotifications(prev =>
|
try {
|
||||||
prev.map(n => n.id === id ? { ...n, read: true } : n)
|
await notificationsApiService.markAsRead(id);
|
||||||
);
|
setNotifications((prev) =>
|
||||||
|
prev.map((n) => (n.id === id ? { ...n, read: true } : n))
|
||||||
|
);
|
||||||
|
setUnreadCount((prev) => Math.max(0, prev - 1));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to mark as read:', err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const markAllAsRead = () => {
|
const markAllAsRead = async () => {
|
||||||
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
|
try {
|
||||||
|
await notificationsApiService.markAllAsRead();
|
||||||
|
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
|
||||||
|
setUnreadCount(0);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to mark all as read:', err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -108,7 +100,9 @@ export default function UserNotificationsPage() {
|
|||||||
Notifications
|
Notifications
|
||||||
</h1>
|
</h1>
|
||||||
<p className="font-serif text-[14px] text-[#00293D]/60">
|
<p className="font-serif text-[14px] text-[#00293D]/60">
|
||||||
{unreadCount > 0 ? `You have ${unreadCount} unread notification${unreadCount > 1 ? 's' : ''}` : 'All caught up!'}
|
{unreadCount > 0
|
||||||
|
? `You have ${unreadCount} unread notification${unreadCount > 1 ? 's' : ''}`
|
||||||
|
: 'All caught up!'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,7 +127,7 @@ export default function UserNotificationsPage() {
|
|||||||
: 'bg-[#00293D]/10 text-[#00293D] hover:bg-[#00293D]/20'
|
: 'bg-[#00293D]/10 text-[#00293D] hover:bg-[#00293D]/20'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
All ({notifications.length})
|
All ({totalCount})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setFilter('unread')}
|
onClick={() => setFilter('unread')}
|
||||||
@@ -150,7 +144,11 @@ export default function UserNotificationsPage() {
|
|||||||
|
|
||||||
{/* Notifications List */}
|
{/* Notifications List */}
|
||||||
<div className="border border-[#00293d]/10 rounded-[20px] bg-white overflow-hidden">
|
<div className="border border-[#00293d]/10 rounded-[20px] bg-white overflow-hidden">
|
||||||
{filteredNotifications.length === 0 ? (
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#e58625]"></div>
|
||||||
|
</div>
|
||||||
|
) : notifications.length === 0 ? (
|
||||||
<div className="p-12 text-center">
|
<div className="p-12 text-center">
|
||||||
<div className="w-16 h-16 rounded-full bg-[#00293D]/5 flex items-center justify-center mx-auto mb-4">
|
<div className="w-16 h-16 rounded-full bg-[#00293D]/5 flex items-center justify-center mx-auto mb-4">
|
||||||
<Image
|
<Image
|
||||||
@@ -167,7 +165,7 @@ export default function UserNotificationsPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-[#00293d]/10">
|
<div className="divide-y divide-[#00293d]/10">
|
||||||
{filteredNotifications.map((notification) => (
|
{notifications.map((notification) => (
|
||||||
<div
|
<div
|
||||||
key={notification.id}
|
key={notification.id}
|
||||||
className={`p-4 hover:bg-[#00293D]/5 transition-colors ${
|
className={`p-4 hover:bg-[#00293D]/5 transition-colors ${
|
||||||
@@ -176,9 +174,11 @@ export default function UserNotificationsPage() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
{/* Icon */}
|
{/* Icon */}
|
||||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
|
<div
|
||||||
!notification.read ? 'bg-[#e58625]/20' : 'bg-[#00293D]/10'
|
className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
|
||||||
}`}>
|
!notification.read ? 'bg-[#e58625]/20' : 'bg-[#00293D]/10'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
src={getNotificationIcon(notification.type)}
|
src={getNotificationIcon(notification.type)}
|
||||||
alt={notification.type}
|
alt={notification.type}
|
||||||
@@ -191,9 +191,13 @@ export default function UserNotificationsPage() {
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className={`font-fractul text-[14px] leading-[17px] ${
|
<p
|
||||||
!notification.read ? 'font-bold text-[#00293D]' : 'font-medium text-[#00293D]/80'
|
className={`font-fractul text-[14px] leading-[17px] ${
|
||||||
}`}>
|
!notification.read
|
||||||
|
? 'font-bold text-[#00293D]'
|
||||||
|
: 'font-medium text-[#00293D]/80'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{notification.title}
|
{notification.title}
|
||||||
</p>
|
</p>
|
||||||
<p className="font-serif text-[14px] text-[#00293D]/60 mt-1">
|
<p className="font-serif text-[14px] text-[#00293D]/60 mt-1">
|
||||||
@@ -201,7 +205,7 @@ export default function UserNotificationsPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-serif text-[12px] text-[#00293D]/50 flex-shrink-0">
|
<span className="font-serif text-[12px] text-[#00293D]/50 flex-shrink-0">
|
||||||
{notification.timestamp}
|
{formatTimestamp(notification.createdAt)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useSession, signOut } from 'next-auth/react';
|
|||||||
import { agentsService } from '@/services/agents.service';
|
import { agentsService } from '@/services/agents.service';
|
||||||
import { usersService } from '@/services/users.service';
|
import { usersService } from '@/services/users.service';
|
||||||
import { uploadService } from '@/services/upload.service';
|
import { uploadService } from '@/services/upload.service';
|
||||||
|
import { notificationsApiService } from '@/services/notifications-api.service';
|
||||||
|
|
||||||
const navLinks = [
|
const navLinks = [
|
||||||
{ label: 'Education', href: '/education' },
|
{ label: 'Education', href: '/education' },
|
||||||
@@ -22,6 +23,7 @@ export function CommonHeader() {
|
|||||||
const profileMenuRef = useRef<HTMLDivElement>(null);
|
const profileMenuRef = useRef<HTMLDivElement>(null);
|
||||||
const [profileImage, setProfileImage] = useState<string | null>(null);
|
const [profileImage, setProfileImage] = useState<string | null>(null);
|
||||||
const [profileName, setProfileName] = useState<string | null>(null);
|
const [profileName, setProfileName] = useState<string | null>(null);
|
||||||
|
const [notificationCount, setNotificationCount] = useState(0);
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
// Close dropdown when clicking outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -80,6 +82,31 @@ export function CommonHeader() {
|
|||||||
}
|
}
|
||||||
}, [session, fetchProfileData]);
|
}, [session, fetchProfileData]);
|
||||||
|
|
||||||
|
// Fetch notification unread count
|
||||||
|
const fetchUnreadCount = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const count = await notificationsApiService.getUnreadCount();
|
||||||
|
setNotificationCount(count);
|
||||||
|
} catch {
|
||||||
|
// Silently fail - user may not be authenticated yet
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (session) {
|
||||||
|
fetchUnreadCount();
|
||||||
|
// Poll every 30 seconds for new notifications
|
||||||
|
const interval = setInterval(fetchUnreadCount, 30000);
|
||||||
|
// Also refresh when a foreground notification arrives
|
||||||
|
const handleNotification = () => fetchUnreadCount();
|
||||||
|
window.addEventListener('notification-received', handleNotification);
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
window.removeEventListener('notification-received', handleNotification);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [session, fetchUnreadCount]);
|
||||||
|
|
||||||
// Listen for profile update events to refresh the header
|
// Listen for profile update events to refresh the header
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleProfileUpdate = () => {
|
const handleProfileUpdate = () => {
|
||||||
@@ -146,8 +173,14 @@ export function CommonHeader() {
|
|||||||
width={20}
|
width={20}
|
||||||
height={22}
|
height={22}
|
||||||
/>
|
/>
|
||||||
{/* Notification Dot */}
|
{/* Notification Badge */}
|
||||||
<span className="absolute -top-0.5 -right-0.5 w-2 h-2 bg-red-500 rounded-full"></span>
|
{notificationCount > 0 && (
|
||||||
|
<span className="absolute -top-1.5 -right-2 min-w-[18px] h-[18px] bg-red-500 rounded-full flex items-center justify-center px-1">
|
||||||
|
<span className="text-white text-[10px] font-bold leading-none">
|
||||||
|
{notificationCount > 99 ? '99+' : notificationCount}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Profile Section with Greeting - Entire area clickable */}
|
{/* Profile Section with Greeting - Entire area clickable */}
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ export function NotificationProvider() {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setToasts((prev) => prev.filter((t) => t.id !== id));
|
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
|
// Dispatch event so the header can refresh unread count
|
||||||
|
window.dispatchEvent(new Event("notification-received"));
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -55,5 +55,7 @@ export { messagesService } from './messages.service';
|
|||||||
// CMS Service
|
// CMS Service
|
||||||
export { cmsService } from './cms.service';
|
export { cmsService } from './cms.service';
|
||||||
|
|
||||||
// Notification Service
|
// Notification Services
|
||||||
export { notificationService } from './notification.service';
|
export { notificationService } from './notification.service';
|
||||||
|
export { notificationsApiService } from './notifications-api.service';
|
||||||
|
export type { AppNotification } from './notifications-api.service';
|
||||||
|
|||||||
53
src/services/notifications-api.service.ts
Normal file
53
src/services/notifications-api.service.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import api from './api';
|
||||||
|
|
||||||
|
export interface AppNotification {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
type: 'connection' | 'message' | 'system' | 'update' | 'request';
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
read: boolean;
|
||||||
|
actionUrl?: string;
|
||||||
|
data?: Record<string, string>;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NotificationsResponse {
|
||||||
|
notifications: AppNotification[];
|
||||||
|
unreadCount: number;
|
||||||
|
pagination: {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
total: number;
|
||||||
|
pages: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationsApiService {
|
||||||
|
async getNotifications(
|
||||||
|
filter: 'all' | 'unread' = 'all',
|
||||||
|
page = 1,
|
||||||
|
limit = 20,
|
||||||
|
): Promise<NotificationsResponse> {
|
||||||
|
const response = await api.get('/notifications', {
|
||||||
|
params: { filter, page, limit },
|
||||||
|
});
|
||||||
|
return response.data?.data || response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUnreadCount(): Promise<number> {
|
||||||
|
const response = await api.get('/notifications/unread-count');
|
||||||
|
const data = response.data?.data || response.data;
|
||||||
|
return data.unreadCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAsRead(notificationId: string): Promise<void> {
|
||||||
|
await api.patch(`/notifications/${notificationId}/read`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAllAsRead(): Promise<void> {
|
||||||
|
await api.patch('/notifications/read-all');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const notificationsApiService = new NotificationsApiService();
|
||||||
Reference in New Issue
Block a user