Bromeo777 commited on
Commit
c1352ff
·
unverified ·
1 Parent(s): 83934bb

Create index.tsx

Browse files
src/components/molecules/NotificationItem/index.tsx ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import {
5
+ CheckCircle2,
6
+ AlertCircle,
7
+ Info,
8
+ Bell,
9
+ Trash2,
10
+ Check,
11
+ Circle
12
+ } from "lucide-react";
13
+ import { Icon } from "@/components/atoms/Icon";
14
+ import { Spinner } from "@/components/atoms/Spinner";
15
+ import { cn } from "@/lib/utils";
16
+
17
+ // 1. Specialized Types for Research Workflows
18
+ export type NotificationType = "success" | "info" | "warning" | "error" | "system";
19
+
20
+ export interface NotificationItemProps {
21
+ id: string;
22
+ type: NotificationType;
23
+ title: string;
24
+ message: string;
25
+ timestamp: string; // Expecting ISO format
26
+ isRead: boolean;
27
+ onRead: (id: string) => Promise<void>;
28
+ onDelete: (id: string) => Promise<void>;
29
+ className?: string;
30
+ }
31
+
32
+ /**
33
+ * NotificationItem Molecule (Highly Optimized)
34
+ * Uses React.memo to prevent unnecessary re-renders in large notification feeds.
35
+ */
36
+ export const NotificationItem = React.memo(({
37
+ id,
38
+ type,
39
+ title,
40
+ message,
41
+ timestamp,
42
+ isRead,
43
+ onRead,
44
+ onDelete,
45
+ className,
46
+ }: NotificationItemProps) => {
47
+ const [isProcessing, setIsProcessing] = React.useState<'read' | 'delete' | null>(null);
48
+
49
+ // 2. Icon Mapping with Semantic Colors
50
+ const getMetadata = () => {
51
+ switch (type) {
52
+ case "success": return { icon: CheckCircle2, color: "text-emerald-500", bg: "bg-emerald-50" };
53
+ case "error": return { icon: AlertCircle, color: "text-destructive", bg: "bg-destructive/5" };
54
+ case "warning": return { icon: AlertCircle, color: "text-amber-500", bg: "bg-amber-50" };
55
+ case "system": return { icon: Bell, color: "text-primary", bg: "bg-primary/5" };
56
+ default: return { icon: Info, color: "text-blue-500", bg: "bg-blue-50" };
57
+ }
58
+ };
59
+
60
+ const { icon, color, bg } = getMetadata();
61
+
62
+ const handleAction = async (action: 'read' | 'delete', fn: (id: string) => Promise<void>) => {
63
+ if (isProcessing) return;
64
+ try {
65
+ setIsProcessing(action);
66
+ await fn(id);
67
+ } catch (error) {
68
+ console.error(`Notification action ${action} failed:`, error);
69
+ } finally {
70
+ setIsProcessing(null);
71
+ }
72
+ };
73
+
74
+ return (
75
+ <div
76
+ role="listitem"
77
+ className={cn(
78
+ "group relative flex items-start gap-4 p-4 transition-all duration-200 border-b last:border-0",
79
+ !isRead ? "bg-primary/[0.03]" : "bg-background",
80
+ isProcessing === 'delete' && "scale-95 opacity-0", // Visual cue for deletion
81
+ className
82
+ )}
83
+ >
84
+ {/* 3. Unread Indicator Dot */}
85
+ {!isRead && (
86
+ <div className="absolute left-1 top-1/2 -translate-y-1/2">
87
+ <Circle className="h-2 w-2 fill-primary text-primary animate-pulse" />
88
+ </div>
89
+ )}
90
+
91
+ {/* 4. Status Icon Wrapper */}
92
+ <div className={cn("mt-1 shrink-0 rounded-full p-2 border shadow-sm", bg, color)}>
93
+ <Icon icon={icon} size={16} />
94
+ </div>
95
+
96
+ {/* 5. Content Area */}
97
+ <div className="flex-1 min-w-0 space-y-1">
98
+ <div className="flex items-center justify-between gap-4">
99
+ <h5 className={cn(
100
+ "text-sm font-semibold leading-none truncate",
101
+ !isRead ? "text-foreground" : "text-muted-foreground"
102
+ )}>
103
+ {title}
104
+ </h5>
105
+ <time className="text-[10px] whitespace-nowrap text-muted-foreground tabular-nums font-medium">
106
+ {/* Logic: Replace with a 'timeago' formatter in the actual implementation */}
107
+ {timestamp}
108
+ </time>
109
+ </div>
110
+
111
+ <p className="text-xs leading-relaxed text-muted-foreground line-clamp-2 break-words">
112
+ {message}
113
+ </p>
114
+ </div>
115
+
116
+ {/* 6. Optimized Action Bar (Reveal on Hover) */}
117
+ <div className="flex items-center gap-1 ml-2">
118
+ {!isRead && (
119
+ <button
120
+ onClick={() => handleAction('read', onRead)}
121
+ disabled={!!isProcessing}
122
+ className="p-2 rounded-md hover:bg-emerald-500/10 text-emerald-600 transition-colors disabled:opacity-50"
123
+ aria-label="Mark as read"
124
+ >
125
+ {isProcessing === 'read' ? <Spinner size={14} /> : <Check size={14} />}
126
+ </button>
127
+ )}
128
+
129
+ <button
130
+ onClick={() => handleAction('delete', onDelete)}
131
+ disabled={!!isProcessing}
132
+ className="p-2 rounded-md hover:bg-destructive/10 text-destructive sm:opacity-0 group-hover:opacity-100 transition-all focus:opacity-100"
133
+ aria-label="Delete notification"
134
+ >
135
+ {isProcessing === 'delete' ? <Spinner size={14} /> : <Trash2 size={14} />}
136
+ </button>
137
+ </div>
138
+ </div>
139
+ );
140
+ });
141
+
142
+ NotificationItem.displayName = "NotificationItem";