1import { useState, useEffect } from "react";
2import { useParams, Link, useLocation } from "react-router-dom";
3import AnnotationCard, { HighlightCard } from "../components/AnnotationCard";
4import BookmarkCard from "../components/BookmarkCard";
5import ReplyList from "../components/ReplyList";
6import {
7 getAnnotation,
8 getReplies,
9 createReply,
10 deleteReply,
11 resolveHandle,
12 normalizeAnnotation,
13} from "../api/client";
14import { useAuth } from "../context/AuthContext";
15import { MessageSquare } from "lucide-react";
16
17export default function AnnotationDetail() {
18 const { uri, did, rkey, handle, type } = useParams();
19 const location = useLocation();
20 const { isAuthenticated, user } = useAuth();
21 const [annotation, setAnnotation] = useState(null);
22 const [replies, setReplies] = useState([]);
23 const [loading, setLoading] = useState(true);
24 const [error, setError] = useState(null);
25
26 const [replyText, setReplyText] = useState("");
27 const [posting, setPosting] = useState(false);
28 const [replyingTo, setReplyingTo] = useState(null);
29
30 const [targetUri, setTargetUri] = useState(uri);
31
32 useEffect(() => {
33 async function resolve() {
34 if (uri) {
35 setTargetUri(uri);
36 return;
37 }
38
39 if (handle && rkey) {
40 let collection = "at.margin.annotation";
41 if (type === "highlight") collection = "at.margin.highlight";
42 if (type === "bookmark") collection = "at.margin.bookmark";
43
44 try {
45 const resolvedDid = await resolveHandle(handle);
46 if (resolvedDid) {
47 setTargetUri(`at://${resolvedDid}/${collection}/${rkey}`);
48 }
49 } catch (e) {
50 console.error("Failed to resolve handle:", e);
51 }
52 } else if (did && rkey) {
53 setTargetUri(`at://${did}/at.margin.annotation/${rkey}`);
54 } else {
55 const pathParts = location.pathname.split("/");
56 const atIndex = pathParts.indexOf("at");
57 if (
58 atIndex !== -1 &&
59 pathParts[atIndex + 1] &&
60 pathParts[atIndex + 2]
61 ) {
62 setTargetUri(
63 `at://${pathParts[atIndex + 1]}/at.margin.annotation/${pathParts[atIndex + 2]}`,
64 );
65 }
66 }
67 }
68 resolve();
69 }, [uri, did, rkey, handle, type, location.pathname]);
70
71 const refreshReplies = async () => {
72 if (!targetUri) return;
73 const repliesData = await getReplies(targetUri);
74 setReplies(repliesData.items || []);
75 };
76
77 useEffect(() => {
78 async function fetchData() {
79 if (!targetUri) return;
80
81 try {
82 setLoading(true);
83 const [annData, repliesData] = await Promise.all([
84 getAnnotation(targetUri),
85 getReplies(targetUri).catch(() => ({ items: [] })),
86 ]);
87 setAnnotation(normalizeAnnotation(annData));
88 setReplies(repliesData.items || []);
89 } catch (err) {
90 setError(err.message);
91 } finally {
92 setLoading(false);
93 }
94 }
95 fetchData();
96 }, [targetUri]);
97
98 const handleReply = async (e) => {
99 if (e) e.preventDefault();
100 if (!replyText.trim()) return;
101
102 try {
103 setPosting(true);
104 const parentUri = replyingTo
105 ? replyingTo.id || replyingTo.uri
106 : targetUri;
107 const parentCid = replyingTo
108 ? replyingTo.cid || ""
109 : annotation?.cid || "";
110
111 await createReply({
112 parentUri,
113 parentCid,
114 rootUri: targetUri,
115 rootCid: annotation?.cid || "",
116 text: replyText,
117 });
118 setReplyText("");
119 setReplyingTo(null);
120 await refreshReplies();
121 } catch (err) {
122 alert("Failed to post reply: " + err.message);
123 } finally {
124 setPosting(false);
125 }
126 };
127
128 const handleDeleteReply = async (reply) => {
129 if (!confirm("Delete this reply?")) return;
130 try {
131 await deleteReply(reply.id || reply.uri);
132 await refreshReplies();
133 } catch (err) {
134 alert("Failed to delete: " + err.message);
135 }
136 };
137
138 if (loading) {
139 return (
140 <div className="annotation-detail-page">
141 <div className="card">
142 <div className="skeleton skeleton-text" style={{ width: "40%" }} />
143 <div className="skeleton skeleton-text" />
144 <div className="skeleton skeleton-text" style={{ width: "60%" }} />
145 </div>
146 </div>
147 );
148 }
149
150 if (error || !annotation) {
151 return (
152 <div className="annotation-detail-page">
153 <div className="empty-state">
154 <div className="empty-state-icon">⚠️</div>
155 <h3 className="empty-state-title">Annotation not found</h3>
156 <p className="empty-state-text">
157 {error || "This annotation may have been deleted."}
158 </p>
159 <Link
160 to="/"
161 className="btn btn-primary"
162 style={{ marginTop: "16px" }}
163 >
164 Back to Feed
165 </Link>
166 </div>
167 </div>
168 );
169 }
170
171 return (
172 <div className="annotation-detail-page">
173 <div className="annotation-detail-header">
174 <Link to="/" className="back-link">
175 ← Back to Feed
176 </Link>
177 </div>
178
179 {annotation.type === "Highlight" ? (
180 <HighlightCard
181 highlight={annotation}
182 onDelete={() => (window.location.href = "/")}
183 />
184 ) : annotation.type === "Bookmark" ? (
185 <BookmarkCard
186 bookmark={annotation}
187 onDelete={() => (window.location.href = "/")}
188 />
189 ) : (
190 <AnnotationCard annotation={annotation} />
191 )}
192
193 {annotation.type !== "Bookmark" && annotation.type !== "Highlight" && (
194 <div className="replies-section">
195 <h3 className="replies-title">
196 <MessageSquare size={18} />
197 Replies ({replies.length})
198 </h3>
199
200 {isAuthenticated && (
201 <div className="reply-form card">
202 {replyingTo && (
203 <div className="replying-to-banner">
204 <span>
205 Replying to @
206 {(replyingTo.creator || replyingTo.author)?.handle ||
207 "unknown"}
208 </span>
209 <button
210 onClick={() => setReplyingTo(null)}
211 className="cancel-reply"
212 >
213 ×
214 </button>
215 </div>
216 )}
217 <textarea
218 value={replyText}
219 onChange={(e) => setReplyText(e.target.value)}
220 placeholder={
221 replyingTo
222 ? `Reply to @${(replyingTo.creator || replyingTo.author)?.handle}...`
223 : "Write a reply..."
224 }
225 className="reply-input"
226 rows={3}
227 disabled={posting}
228 />
229 <div className="reply-form-actions">
230 <button
231 className="btn btn-primary"
232 disabled={posting || !replyText.trim()}
233 onClick={() => handleReply()}
234 >
235 {posting ? "Posting..." : "Reply"}
236 </button>
237 </div>
238 </div>
239 )}
240
241 <ReplyList
242 replies={replies}
243 rootUri={targetUri}
244 user={user}
245 onReply={(reply) => setReplyingTo(reply)}
246 onDelete={handleDeleteReply}
247 isInline={false}
248 />
249 </div>
250 )}
251 </div>
252 );
253}