({ notification, onMarkRead }: NotificationItemProps)
| 29 | } as const; |
| 30 | |
| 31 | export function NotificationItem({ notification, onMarkRead }: NotificationItemProps) { |
| 32 | const config = CATEGORY_CONFIG[notification.category]; |
| 33 | const Icon = config.icon; |
| 34 | |
| 35 | const handleClick = () => { |
| 36 | if (!notification.read) { |
| 37 | onMarkRead(notification.id); |
| 38 | } |
| 39 | }; |
| 40 | |
| 41 | const content = ( |
| 42 | <div |
| 43 | className={cn( |
| 44 | "flex items-start gap-3 px-4 py-3 transition-colors cursor-pointer", |
| 45 | "hover:bg-surface-800/60", |
| 46 | !notification.read && "bg-surface-800/30" |
| 47 | )} |
| 48 | onClick={handleClick} |
| 49 | > |
| 50 | {/* Icon */} |
| 51 | <div |
| 52 | className={cn( |
| 53 | "mt-0.5 shrink-0 w-7 h-7 rounded-full flex items-center justify-center", |
| 54 | config.bgColor |
| 55 | )} |
| 56 | > |
| 57 | <Icon className={cn("w-3.5 h-3.5", config.iconColor)} /> |
| 58 | </div> |
| 59 | |
| 60 | {/* Content */} |
| 61 | <div className="flex-1 min-w-0"> |
| 62 | <div className="flex items-start justify-between gap-2"> |
| 63 | <p |
| 64 | className={cn( |
| 65 | "text-sm leading-snug", |
| 66 | notification.read ? "text-surface-300" : "text-surface-100 font-medium" |
| 67 | )} |
| 68 | > |
| 69 | {notification.title} |
| 70 | </p> |
| 71 | <div className="flex items-center gap-1.5 shrink-0"> |
| 72 | {!notification.read && ( |
| 73 | <span className="w-1.5 h-1.5 rounded-full bg-brand-500 mt-1" /> |
| 74 | )} |
| 75 | {notification.link && ( |
| 76 | <ExternalLink className="w-3 h-3 text-surface-500" /> |
| 77 | )} |
| 78 | </div> |
| 79 | </div> |
| 80 | <p className="text-xs text-surface-500 mt-0.5 leading-relaxed line-clamp-2"> |
| 81 | {notification.description} |
| 82 | </p> |
| 83 | <p className="text-xs text-surface-600 mt-1"> |
| 84 | {formatDate(notification.createdAt)} |
| 85 | </p> |
| 86 | </div> |
| 87 | </div> |
| 88 | ); |
nothing calls this directly
no test coverage detected