notifciations issues fixed final

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-28 00:52:14 +00:00
parent 28a60f8141
commit 0605f650b1
12 changed files with 1384 additions and 18 deletions

View File

@@ -0,0 +1,434 @@
import { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet-async';
import { Link } from 'react-router-dom';
import {
Bell,
CheckCircle,
AlertTriangle,
XCircle,
Info,
Trash2,
CheckCheck,
Filter,
Calendar,
Globe,
} from 'lucide-react';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import { useNotificationStore } from '../../store/notificationStore';
import type { NotificationAPI } from '../../services/notifications.api';
import { deleteNotification as deleteNotificationAPI } from '../../services/notifications.api';
interface FilterState {
severity: string;
notification_type: string;
is_read: string;
}
export default function NotificationsPage() {
const [apiNotifications, setApiNotifications] = useState<NotificationAPI[]>([]);
const [loading, setLoading] = useState(true);
const {
unreadCount,
fetchNotifications: storeFetchNotifications,
markAsRead: storeMarkAsRead,
markAllAsRead: storeMarkAllAsRead,
syncUnreadCount,
} = useNotificationStore();
const [filters, setFilters] = useState<FilterState>({
severity: '',
notification_type: '',
is_read: '',
});
const [showFilters, setShowFilters] = useState(false);
useEffect(() => {
loadNotifications();
}, []);
const loadNotifications = async () => {
setLoading(true);
try {
// Import here to avoid circular dependencies
const { fetchNotifications } = await import('../../services/notifications.api');
const data = await fetchNotifications();
setApiNotifications(data.results);
await syncUnreadCount();
} catch (error) {
console.error('Failed to load notifications:', error);
} finally {
setLoading(false);
}
};
// Get severity icon and color
const getSeverityIcon = (severity: string) => {
switch (severity) {
case 'success':
return <CheckCircle className="w-5 h-5 text-green-500" />;
case 'warning':
return <AlertTriangle className="w-5 h-5 text-yellow-500" />;
case 'error':
return <XCircle className="w-5 h-5 text-red-500" />;
default:
return <Info className="w-5 h-5 text-blue-500" />;
}
};
// Get notification type label
const getTypeLabel = (type: string) => {
return type
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
// Format timestamp
const formatTimestamp = (timestamp: string) => {
const date = new Date(timestamp);
const now = new Date();
const diff = now.getTime() - date.getTime();
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 7) {
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit'
});
} else if (days > 0) {
return `${days} day${days !== 1 ? 's' : ''} ago`;
} else if (hours > 0) {
return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
} else if (minutes > 0) {
return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
} else {
return 'Just now';
}
};
// Handle notification click
const handleNotificationClick = async (id: number, isRead: boolean) => {
if (!isRead) {
try {
const { markNotificationRead } = await import('../../services/notifications.api');
await markNotificationRead(id);
storeMarkAsRead(`api_${id}`);
await loadNotifications();
} catch (error) {
console.error('Failed to mark notification as read:', error);
}
}
};
// Handle delete
const handleDelete = async (id: number) => {
if (window.confirm('Delete this notification?')) {
try {
await deleteNotificationAPI(id);
await loadNotifications();
} catch (error) {
console.error('Failed to delete notification:', error);
}
}
};
// Handle mark all read
const handleMarkAllRead = async () => {
try {
const { markAllNotificationsRead } = await import('../../services/notifications.api');
await markAllNotificationsRead();
await storeMarkAllAsRead();
await loadNotifications();
} catch (error) {
console.error('Failed to mark all as read:', error);
}
};
// Filter notifications
const filteredNotifications = apiNotifications.filter((notification) => {
if (filters.severity && notification.severity !== filters.severity) {
return false;
}
if (
filters.notification_type &&
notification.notification_type !== filters.notification_type
) {
return false;
}
if (filters.is_read !== '' && String(notification.is_read) !== filters.is_read) {
return false;
}
return true;
});
// Get unique notification types for filter
const notificationTypes = Array.from(
new Set(apiNotifications.map((n) => n.notification_type))
);
return (
<>
<Helmet>
<title>Notifications - IGNY8</title>
</Helmet>
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Notifications
</h1>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{unreadCount > 0 ? (
<span className="font-medium text-blue-600 dark:text-blue-400">
{unreadCount} unread notification{unreadCount !== 1 ? 's' : ''}
</span>
) : (
'All caught up!'
)}
</p>
</div>
<div className="flex items-center gap-3">
<Button
variant="outline"
size="sm"
onClick={() => setShowFilters(!showFilters)}
className="flex items-center gap-2"
>
<Filter className="w-4 h-4" />
Filters
</Button>
{unreadCount > 0 && (
<Button
variant="outline"
size="sm"
onClick={handleMarkAllRead}
className="flex items-center gap-2"
>
<CheckCheck className="w-4 h-4" />
Mark All Read
</Button>
)}
</div>
</div>
{/* Filters */}
{showFilters && (
<Card className="p-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Severity Filter */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Severity
</label>
<select
value={filters.severity}
onChange={(e) =>
setFilters({ ...filters, severity: e.target.value })
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
>
<option value="">All</option>
<option value="info">Info</option>
<option value="success">Success</option>
<option value="warning">Warning</option>
<option value="error">Error</option>
</select>
</div>
{/* Type Filter */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Type
</label>
<select
value={filters.notification_type}
onChange={(e) =>
setFilters({ ...filters, notification_type: e.target.value })
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
>
<option value="">All</option>
{notificationTypes.map((type) => (
<option key={type} value={type}>
{getTypeLabel(type)}
</option>
))}
</select>
</div>
{/* Read Status Filter */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status
</label>
<select
value={filters.is_read}
onChange={(e) =>
setFilters({ ...filters, is_read: e.target.value })
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
>
<option value="">All</option>
<option value="false">Unread</option>
<option value="true">Read</option>
</select>
</div>
</div>
{/* Clear Filters */}
{Object.values(filters).some((v) => v !== '') && (
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<Button
variant="ghost"
size="sm"
onClick={() =>
setFilters({
severity: '',
notification_type: '',
is_read: '',
})
}
>
Clear All Filters
</Button>
</div>
)}
</Card>
)}
{/* Notifications List */}
<Card className="overflow-hidden">
{loading ? (
<div className="flex items-center justify-center p-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
) : filteredNotifications.length === 0 ? (
<div className="text-center p-12">
<Bell className="w-12 h-12 text-gray-400 mx-auto mb-3" />
<p className="text-gray-600 dark:text-gray-400">
{apiNotifications.length === 0
? 'No notifications yet'
: 'No notifications match your filters'}
</p>
</div>
) : (
<div className="divide-y divide-gray-200 dark:divide-gray-700">
{filteredNotifications.map((notification) => (
<div
key={notification.id}
className={`p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors ${
!notification.is_read ? 'bg-blue-50 dark:bg-blue-900/10' : ''
}`}
>
<div className="flex items-start gap-4">
{/* Icon */}
<div className="flex-shrink-0 mt-1">
{getSeverityIcon(notification.severity)}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-4">
<div>
<h3
className={`text-base font-medium ${
notification.is_read
? 'text-gray-900 dark:text-white'
: 'text-blue-900 dark:text-blue-100'
}`}
>
{notification.title}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{notification.message}
</p>
{/* Metadata */}
<div className="flex flex-wrap items-center gap-3 mt-2 text-xs text-gray-500 dark:text-gray-500">
<span className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
{formatTimestamp(notification.created_at)}
</span>
<span className="px-2 py-0.5 bg-gray-100 dark:bg-gray-700 rounded">
{getTypeLabel(notification.notification_type)}
</span>
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-2 flex-shrink-0">
{!notification.is_read && (
<Button
variant="ghost"
size="sm"
onClick={() =>
handleNotificationClick(notification.id, false)
}
>
<CheckCircle className="w-4 h-4" />
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(notification.id)}
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
{/* Action Button */}
{notification.action_url && notification.action_label && (
<div className="mt-3">
<Link to={notification.action_url}>
<Button
variant="outline"
size="sm"
onClick={() =>
handleNotificationClick(
notification.id,
notification.is_read
)
}
>
{notification.action_label}
</Button>
</Link>
</div>
)}
</div>
</div>
</div>
))}
</div>
)}
</Card>
{/* Footer Info */}
{filteredNotifications.length > 0 && (
<div className="text-center text-sm text-gray-500 dark:text-gray-500">
Showing {filteredNotifications.length} of {apiNotifications.length}{' '}
notification{apiNotifications.length !== 1 ? 's' : ''}
</div>
)}
</div>
</>
);
}