264 lines
9.5 KiB
TypeScript
264 lines
9.5 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import Image from 'next/image';
|
|
import Link from 'next/link';
|
|
import { notificationsApiService, type AppNotification } from '@/services/notifications-api.service';
|
|
|
|
function getNotificationIcon(type: AppNotification['type']) {
|
|
switch (type) {
|
|
case 'connection':
|
|
return '/assets/icons/user-placeholder-icon.svg';
|
|
case 'message':
|
|
return '/assets/icons/message-icon.svg';
|
|
case 'system':
|
|
return '/assets/icons/settings-icon.svg';
|
|
case 'update':
|
|
return '/assets/icons/notification-bell-icon.svg';
|
|
default:
|
|
return '/assets/icons/notification-bell-icon.svg';
|
|
}
|
|
}
|
|
|
|
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() {
|
|
const [notifications, setNotifications] = useState<AppNotification[]>([]);
|
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
const [totalCount, setTotalCount] = useState(0);
|
|
const [filter, setFilter] = useState<'all' | 'unread'>('all');
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
const fetchNotifications = useCallback(async () => {
|
|
try {
|
|
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]);
|
|
|
|
useEffect(() => {
|
|
fetchNotifications();
|
|
}, [fetchNotifications]);
|
|
|
|
const markAsRead = async (id: string) => {
|
|
try {
|
|
await notificationsApiService.markAsRead(id);
|
|
if (filter === 'unread') {
|
|
setNotifications((prev) => prev.filter((n) => n.id !== id));
|
|
} else {
|
|
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 = async () => {
|
|
try {
|
|
await notificationsApiService.markAllAsRead();
|
|
if (filter === 'unread') {
|
|
setNotifications([]);
|
|
} else {
|
|
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
|
|
}
|
|
setUnreadCount(0);
|
|
} catch (err) {
|
|
console.error('Failed to mark all as read:', err);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-7xl mx-auto px-4 lg:px-8 py-6">
|
|
{/* Header */}
|
|
<div className="border border-[#00293d]/10 rounded-[20px] bg-white p-6 mb-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-12 h-12 rounded-full bg-[#e58625]/10 flex items-center justify-center">
|
|
<Image
|
|
src="/assets/icons/notification-bell-icon.svg"
|
|
alt="Notifications"
|
|
width={24}
|
|
height={24}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<h1 className="font-fractul font-bold text-[24px] leading-[29px] text-[#00293D]">
|
|
Notifications
|
|
</h1>
|
|
<p className="font-serif text-[14px] text-[#00293D]/60">
|
|
{unreadCount > 0
|
|
? `You have ${unreadCount} unread notification${unreadCount > 1 ? 's' : ''}`
|
|
: 'All caught up!'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{unreadCount > 0 && (
|
|
<button
|
|
onClick={markAllAsRead}
|
|
className="font-serif text-[14px] text-[#e58625] hover:text-[#d47720] transition-colors"
|
|
>
|
|
Mark all as read
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Filter Tabs */}
|
|
<div className="flex gap-4 mt-6">
|
|
<button
|
|
onClick={() => setFilter('all')}
|
|
className={`px-4 py-2 rounded-[15px] font-serif text-[14px] transition-colors ${
|
|
filter === 'all'
|
|
? 'bg-[#00293D] text-white'
|
|
: 'bg-[#00293D]/10 text-[#00293D] hover:bg-[#00293D]/20'
|
|
}`}
|
|
>
|
|
All ({totalCount})
|
|
</button>
|
|
<button
|
|
onClick={() => setFilter('unread')}
|
|
className={`px-4 py-2 rounded-[15px] font-serif text-[14px] transition-colors ${
|
|
filter === 'unread'
|
|
? 'bg-[#00293D] text-white'
|
|
: 'bg-[#00293D]/10 text-[#00293D] hover:bg-[#00293D]/20'
|
|
}`}
|
|
>
|
|
Unread ({unreadCount})
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Notifications List */}
|
|
<div className="border border-[#00293d]/10 rounded-[20px] bg-white overflow-hidden">
|
|
{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="w-16 h-16 rounded-full bg-[#00293D]/5 flex items-center justify-center mx-auto mb-4">
|
|
<Image
|
|
src="/assets/icons/notification-bell-icon.svg"
|
|
alt="No notifications"
|
|
width={32}
|
|
height={32}
|
|
className="opacity-30"
|
|
/>
|
|
</div>
|
|
<p className="font-serif text-[16px] text-[#00293D]/50">
|
|
{filter === 'unread' ? 'No unread notifications' : 'No notifications yet'}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="divide-y divide-[#00293d]/10">
|
|
{notifications.map((notification) => (
|
|
<div
|
|
key={notification.id}
|
|
className={`p-4 hover:bg-[#00293D]/5 transition-colors ${
|
|
!notification.read ? 'bg-[#e58625]/5' : ''
|
|
}`}
|
|
>
|
|
<div className="flex items-start gap-4">
|
|
{/* Icon */}
|
|
<div
|
|
className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
|
|
!notification.read ? 'bg-[#e58625]/20' : 'bg-[#00293D]/10'
|
|
}`}
|
|
>
|
|
<Image
|
|
src={getNotificationIcon(notification.type)}
|
|
alt={notification.type}
|
|
width={20}
|
|
height={20}
|
|
/>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div>
|
|
<p
|
|
className={`font-fractul text-[14px] leading-[17px] ${
|
|
!notification.read
|
|
? 'font-bold text-[#00293D]'
|
|
: 'font-medium text-[#00293D]/80'
|
|
}`}
|
|
>
|
|
{notification.title}
|
|
</p>
|
|
<p className="font-serif text-[14px] text-[#00293D]/60 mt-1">
|
|
{notification.description}
|
|
</p>
|
|
</div>
|
|
<span className="font-serif text-[12px] text-[#00293D]/50 flex-shrink-0">
|
|
{formatTimestamp(notification.createdAt)}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex items-center gap-4 mt-3">
|
|
{notification.actionUrl && (
|
|
<Link
|
|
href={notification.actionUrl}
|
|
className="font-serif text-[13px] text-[#e58625] hover:text-[#d47720] transition-colors"
|
|
>
|
|
View details
|
|
</Link>
|
|
)}
|
|
{!notification.read && (
|
|
<button
|
|
onClick={() => markAsRead(notification.id)}
|
|
className="font-serif text-[13px] text-[#00293D]/50 hover:text-[#00293D] transition-colors"
|
|
>
|
|
Mark as read
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Unread indicator */}
|
|
{!notification.read && (
|
|
<div className="w-2 h-2 rounded-full bg-[#e58625] flex-shrink-0 mt-2" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Settings Link */}
|
|
<div className="mt-6 text-center">
|
|
<Link
|
|
href="/user/settings/notifications"
|
|
className="font-serif text-[14px] text-[#00293D]/60 hover:text-[#00293D] transition-colors"
|
|
>
|
|
Manage notification settings
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|