localgreenchain/components/notifications/NotificationItem.tsx
Claude 62c1ded598
Add comprehensive notification system (Agent 8)
Implement multi-channel notification system with:
- Core notification service with email, push, and in-app channels
- Email templates for all notification types (welcome, plant registered,
  transport alerts, farm alerts, harvest ready, demand matches, weekly digest)
- Push notification support with VAPID authentication
- In-app notification management with read/unread tracking
- Notification scheduler for recurring and scheduled notifications
- API endpoints for notifications CRUD, preferences, and subscriptions
- UI components (NotificationBell, NotificationList, NotificationItem,
  PreferencesForm)
- Full notifications page with preferences management
- Service worker for push notification handling
2025-11-23 03:52:41 +00:00

156 lines
5 KiB
TypeScript

/**
* NotificationItem Component
* Single notification display with actions
*/
import React from 'react';
interface Notification {
id: string;
type: string;
title: string;
message: string;
actionUrl?: string;
read: boolean;
createdAt: string;
}
interface NotificationItemProps {
notification: Notification;
onMarkAsRead: (id: string) => void;
onDelete: (id: string) => void;
compact?: boolean;
}
const typeIcons: Record<string, { icon: string; bgColor: string }> = {
welcome: { icon: '👋', bgColor: 'bg-blue-100' },
plant_registered: { icon: '🌱', bgColor: 'bg-green-100' },
plant_reminder: { icon: '🌿', bgColor: 'bg-green-100' },
transport_alert: { icon: '🚚', bgColor: 'bg-yellow-100' },
farm_alert: { icon: '🏭', bgColor: 'bg-orange-100' },
harvest_ready: { icon: '🎉', bgColor: 'bg-green-100' },
demand_match: { icon: '🤝', bgColor: 'bg-purple-100' },
weekly_digest: { icon: '📊', bgColor: 'bg-blue-100' },
system_alert: { icon: '⚙️', bgColor: 'bg-gray-100' }
};
export function NotificationItem({
notification,
onMarkAsRead,
onDelete,
compact = false
}: NotificationItemProps) {
const { icon, bgColor } = typeIcons[notification.type] || typeIcons.system_alert;
function formatTimeAgo(dateString: string): string {
const date = new Date(dateString);
const now = new Date();
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
if (seconds < 60) return 'Just now';
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
if (seconds < 604800) return `${Math.floor(seconds / 86400)}d ago`;
return date.toLocaleDateString();
}
function handleClick() {
if (!notification.read) {
onMarkAsRead(notification.id);
}
if (notification.actionUrl) {
window.location.href = notification.actionUrl;
}
}
return (
<div
className={`relative group ${compact ? 'p-3' : 'p-4'} hover:bg-gray-50 transition-colors ${
!notification.read ? 'bg-green-50/30' : ''
}`}
>
<div
className="flex items-start cursor-pointer"
onClick={handleClick}
role="button"
tabIndex={0}
onKeyPress={e => e.key === 'Enter' && handleClick()}
>
{/* Icon */}
<div
className={`flex-shrink-0 ${compact ? 'w-8 h-8' : 'w-10 h-10'} ${bgColor} rounded-full flex items-center justify-center`}
>
<span className={compact ? 'text-sm' : 'text-lg'}>{icon}</span>
</div>
{/* Content */}
<div className={`flex-1 ${compact ? 'ml-3' : 'ml-4'}`}>
<div className="flex items-start justify-between">
<div className="flex-1">
<p
className={`font-medium text-gray-900 ${compact ? 'text-sm' : ''} ${
!notification.read ? 'font-semibold' : ''
}`}
>
{notification.title}
</p>
<p className={`text-gray-600 mt-0.5 ${compact ? 'text-xs line-clamp-2' : 'text-sm'}`}>
{notification.message}
</p>
</div>
{/* Unread indicator */}
{!notification.read && (
<div className="flex-shrink-0 ml-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
</div>
)}
</div>
<div className="flex items-center mt-2">
<span className={`text-gray-400 ${compact ? 'text-xs' : 'text-xs'}`}>
{formatTimeAgo(notification.createdAt)}
</span>
{notification.actionUrl && (
<span className={`ml-2 text-green-600 ${compact ? 'text-xs' : 'text-xs'}`}>
View details
</span>
)}
</div>
</div>
</div>
{/* Actions (visible on hover) */}
<div className="absolute right-4 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity flex space-x-1">
{!notification.read && (
<button
onClick={e => {
e.stopPropagation();
onMarkAsRead(notification.id);
}}
className="p-1.5 text-gray-400 hover:text-green-600 hover:bg-green-50 rounded"
title="Mark as read"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</button>
)}
<button
onClick={e => {
e.stopPropagation();
onDelete(notification.id);
}}
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded"
title="Delete"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
);
}