1import { useState, useEffect } from "react";
2import { Link } from "react-router-dom";
3import { useAuth } from "../context/AuthContext";
4import { getNotifications, markNotificationsRead } from "../api/client";
5import { BellIcon, HeartIcon, ReplyIcon } from "../components/Icons";
6
7function getNotificationRoute(n) {
8 if (n.type === "reply" && n.subject?.inReplyTo) {
9 return `/annotation/${encodeURIComponent(n.subject.inReplyTo)}`;
10 }
11 if (!n.subjectUri) return "/";
12 if (n.subjectUri.includes("at.margin.bookmark")) {
13 return `/bookmarks`;
14 }
15 if (n.subjectUri.includes("at.margin.highlight")) {
16 return `/highlights`;
17 }
18 return `/annotation/${encodeURIComponent(n.subjectUri)}`;
19}
20
21export default function Notifications() {
22 const { user } = useAuth();
23 const [notifications, setNotifications] = useState([]);
24 const [loading, setLoading] = useState(true);
25 const [error, setError] = useState(null);
26
27 useEffect(() => {
28 if (!user?.did) return;
29
30 async function load() {
31 try {
32 setLoading(true);
33 const data = await getNotifications();
34 setNotifications(data.items || []);
35 await markNotificationsRead();
36 } catch (err) {
37 setError(err.message);
38 } finally {
39 setLoading(false);
40 }
41 }
42 load();
43 }, [user?.did]);
44
45 const formatTime = (dateStr) => {
46 const date = new Date(dateStr);
47 const now = new Date();
48 const diffMs = now - date;
49 const diffMins = Math.floor(diffMs / 60000);
50 const diffHours = Math.floor(diffMs / 3600000);
51 const diffDays = Math.floor(diffMs / 86400000);
52
53 if (diffMins < 1) return "just now";
54 if (diffMins < 60) return `${diffMins}m ago`;
55 if (diffHours < 24) return `${diffHours}h ago`;
56 if (diffDays < 7) return `${diffDays}d ago`;
57 return date.toLocaleDateString();
58 };
59
60 const getNotificationIcon = (type) => {
61 switch (type) {
62 case "like":
63 return <HeartIcon size={16} />;
64 case "reply":
65 return <ReplyIcon size={16} />;
66 default:
67 return <BellIcon size={16} />;
68 }
69 };
70
71 const getNotificationText = (n) => {
72 const name = n.actor?.displayName || n.actor?.handle || "Unknown";
73 const handle = n.actor?.handle;
74
75 switch (n.type) {
76 case "like":
77 return (
78 <span>
79 <Link
80 to={`/profile/${handle}`}
81 className="notification-author-link"
82 onClick={(e) => e.stopPropagation()}
83 >
84 {name}
85 </Link>{" "}
86 liked your annotation
87 </span>
88 );
89 case "reply":
90 return (
91 <span>
92 <Link
93 to={`/profile/${handle}`}
94 className="notification-author-link"
95 onClick={(e) => e.stopPropagation()}
96 >
97 {name}
98 </Link>{" "}
99 replied to your annotation
100 </span>
101 );
102 default:
103 return (
104 <span>
105 <Link
106 to={`/profile/${handle}`}
107 className="notification-author-link"
108 onClick={(e) => e.stopPropagation()}
109 >
110 {name}
111 </Link>{" "}
112 interacted with your content
113 </span>
114 );
115 }
116 };
117
118 if (!user) {
119 return (
120 <div className="notifications-page">
121 <div className="page-header">
122 <h1 className="page-title">Notifications</h1>
123 </div>
124 <div className="empty-state">
125 <BellIcon size={48} />
126 <h3>Sign in to see notifications</h3>
127 <p>Get notified when people like or reply to your content</p>
128 </div>
129 </div>
130 );
131 }
132
133 return (
134 <div className="notifications-page">
135 <div className="page-header">
136 <h1 className="page-title">Notifications</h1>
137 <p className="page-description">
138 Likes and replies on your annotations
139 </p>
140 </div>
141
142 {loading && (
143 <div className="loading-container">
144 <div className="loading-spinner"></div>
145 </div>
146 )}
147
148 {error && (
149 <div className="error-message">
150 <p>Error: {error}</p>
151 </div>
152 )}
153
154 {!loading && !error && notifications.length === 0 && (
155 <div className="empty-state">
156 <BellIcon size={48} />
157 <h3>No notifications yet</h3>
158 <p>
159 When someone likes or replies to your content, you'll see it
160 here
161 </p>
162 </div>
163 )}
164
165 {!loading && !error && notifications.length > 0 && (
166 <div className="notifications-list">
167 {notifications.map((n, i) => (
168 <Link
169 key={n.id || i}
170 to={getNotificationRoute(n)}
171 className="notification-item card"
172 style={{ alignItems: "center" }}
173 >
174 <div
175 className="notification-avatar-container"
176 style={{ marginRight: 12, position: "relative" }}
177 >
178 {n.actor?.avatar ? (
179 <img
180 src={n.actor.avatar}
181 alt={n.actor.handle}
182 style={{
183 width: 40,
184 height: 40,
185 borderRadius: "50%",
186 objectFit: "cover",
187 }}
188 />
189 ) : (
190 <div
191 style={{
192 width: 40,
193 height: 40,
194 borderRadius: "50%",
195 background: "#eee",
196 display: "flex",
197 alignItems: "center",
198 justifyContent: "center",
199 }}
200 >
201 {(n.actor?.handle || "?")[0].toUpperCase()}
202 </div>
203 )}
204 <div
205 className="notification-icon-badge"
206 data-type={n.type}
207 style={{
208 position: "absolute",
209 bottom: -4,
210 right: -4,
211 background: "var(--bg-primary)",
212 borderRadius: "50%",
213 padding: 2,
214 display: "flex",
215 boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
216 }}
217 >
218 {getNotificationIcon(n.type)}
219 </div>
220 </div>
221 <div className="notification-content">
222 <p className="notification-text">{getNotificationText(n)}</p>
223 <span className="notification-time">
224 {formatTime(n.createdAt)}
225 </span>
226 </div>
227 </Link>
228 ))}
229 </div>
230 )}
231 </div>
232 );
233}