Files
frontend/src/app/(user)/user/notifications/page.tsx

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>
);
}