1import { useState, useEffect, useCallback } from "react";
2import { useParams, useNavigate, Link, useLocation } from "react-router-dom";
3import { ArrowLeft, Edit2, Trash2, Plus } from "lucide-react";
4import {
5 getCollections,
6 getCollectionItems,
7 removeItemFromCollection,
8 deleteCollection,
9 resolveHandle,
10} from "../api/client";
11import { useAuth } from "../context/AuthContext";
12import CollectionModal from "../components/CollectionModal";
13import CollectionIcon from "../components/CollectionIcon";
14import AnnotationCard, { HighlightCard } from "../components/AnnotationCard";
15import BookmarkCard from "../components/BookmarkCard";
16import ShareMenu from "../components/ShareMenu";
17
18export default function CollectionDetail() {
19 const { rkey, handle, "*": wildcardPath } = useParams();
20 const location = useLocation();
21 const navigate = useNavigate();
22 const { user } = useAuth();
23
24 const [collection, setCollection] = useState(null);
25 const [items, setItems] = useState([]);
26 const [loading, setLoading] = useState(true);
27 const [error, setError] = useState(null);
28 const [isEditModalOpen, setIsEditModalOpen] = useState(false);
29
30 const searchParams = new URLSearchParams(location.search);
31 const paramAuthorDid = searchParams.get("author");
32
33 const isOwner =
34 user?.did &&
35 (collection?.creator?.did === user.did || paramAuthorDid === user.did);
36
37 const fetchContext = useCallback(async () => {
38 try {
39 setLoading(true);
40
41 let targetUri = null;
42 let targetDid = paramAuthorDid || user?.did;
43
44 if (handle && rkey) {
45 try {
46 targetDid = await resolveHandle(handle);
47 targetUri = `at://${targetDid}/at.margin.collection/${rkey}`;
48 } catch (e) {
49 console.error("Failed to resolve handle", e);
50 }
51 } else if (wildcardPath) {
52 targetUri = decodeURIComponent(wildcardPath);
53 } else if (rkey && targetDid) {
54 targetUri = `at://${targetDid}/at.margin.collection/${rkey}`;
55 }
56
57 if (!targetUri) {
58 if (!user && !handle && !paramAuthorDid) {
59 setError("Please log in to view your collections");
60 return;
61 }
62 setError("Invalid collection URL");
63 return;
64 }
65
66 if (!targetDid && targetUri.startsWith("at://")) {
67 const parts = targetUri.split("/");
68 if (parts.length > 2) targetDid = parts[2];
69 }
70
71 if (!targetDid) {
72 setError("Could not determine collection owner");
73 return;
74 }
75
76 const [cols, itemsData] = await Promise.all([
77 getCollections(targetDid),
78 getCollectionItems(targetUri),
79 ]);
80
81 const found =
82 cols.items?.find((c) => c.uri === targetUri) ||
83 cols.items?.find(
84 (c) => targetUri && c.uri.endsWith(targetUri.split("/").pop()),
85 );
86
87 if (!found) {
88 setError("Collection not found");
89 return;
90 }
91 setCollection(found);
92 setItems(itemsData || []);
93 } catch (err) {
94 console.error(err);
95 setError("Failed to load collection");
96 } finally {
97 setLoading(false);
98 }
99 }, [paramAuthorDid, user, handle, rkey, wildcardPath]);
100
101 useEffect(() => {
102 fetchContext();
103 }, [fetchContext]);
104
105 const handleEditSuccess = () => {
106 fetchContext();
107 setIsEditModalOpen(false);
108 };
109
110 const handleDeleteItem = async (itemUri) => {
111 if (!confirm("Remove this item from the collection?")) return;
112 try {
113 await removeItemFromCollection(itemUri);
114 setItems((prev) => prev.filter((i) => i.uri !== itemUri));
115 } catch (err) {
116 console.error(err);
117 alert("Failed to remove item");
118 }
119 };
120
121 if (loading) {
122 return (
123 <div className="feed-page">
124 <div
125 style={{
126 display: "flex",
127 justifyContent: "center",
128 padding: "60px 0",
129 }}
130 >
131 <div className="spinner"></div>
132 </div>
133 </div>
134 );
135 }
136
137 if (error || !collection) {
138 return (
139 <div className="feed-page">
140 <div className="empty-state card">
141 <div className="empty-state-icon">⚠️</div>
142 <h3 className="empty-state-title">
143 {error || "Collection not found"}
144 </h3>
145 <button
146 onClick={() => navigate("/collections")}
147 className="btn btn-secondary"
148 style={{ marginTop: "16px" }}
149 >
150 Back to Collections
151 </button>
152 </div>
153 </div>
154 );
155 }
156
157 return (
158 <div className="feed-page">
159 <Link to="/collections" className="back-link">
160 <ArrowLeft size={18} />
161 <span>Collections</span>
162 </Link>
163
164 <div className="collection-detail-header">
165 <div className="collection-detail-icon">
166 <CollectionIcon icon={collection.icon} size={28} />
167 </div>
168 <div className="collection-detail-info">
169 <h1 className="collection-detail-title">{collection.name}</h1>
170 {collection.description && (
171 <p className="collection-detail-desc">{collection.description}</p>
172 )}
173 <div className="collection-detail-stats">
174 <span>
175 {items.length} {items.length === 1 ? "item" : "items"}
176 </span>
177 <span>·</span>
178 <span>
179 Created {new Date(collection.createdAt).toLocaleDateString()}
180 </span>
181 </div>
182 </div>
183 <div className="collection-detail-actions">
184 <ShareMenu
185 uri={collection.uri}
186 handle={collection.creator?.handle}
187 type="Collection"
188 text={`Check out this collection: ${collection.name}`}
189 />
190 {isOwner && (
191 <>
192 <button
193 onClick={() => setIsEditModalOpen(true)}
194 className="collection-detail-edit"
195 title="Edit Collection"
196 >
197 <Edit2 size={18} />
198 </button>
199 <button
200 onClick={async () => {
201 if (confirm("Delete this collection and all its items?")) {
202 await deleteCollection(collection.uri);
203 navigate("/collections");
204 }
205 }}
206 className="collection-detail-delete"
207 title="Delete Collection"
208 >
209 <Trash2 size={18} />
210 </button>
211 </>
212 )}
213 </div>
214 </div>
215
216 <div className="feed">
217 {items.length === 0 ? (
218 <div className="empty-state card" style={{ borderStyle: "dashed" }}>
219 <div className="empty-state-icon">
220 <Plus size={32} />
221 </div>
222 <h3 className="empty-state-title">Collection is empty</h3>
223 <p className="empty-state-text">
224 {isOwner
225 ? 'Add items to this collection from your feed or bookmarks using the "Collect" button.'
226 : "This collection has no items yet."}
227 </p>
228 </div>
229 ) : (
230 items.map((item) => (
231 <div key={item.uri} className="collection-item-wrapper">
232 {isOwner && (
233 <button
234 onClick={() => handleDeleteItem(item.uri)}
235 className="collection-item-remove"
236 title="Remove from collection"
237 >
238 <Trash2 size={14} />
239 </button>
240 )}
241
242 {item.annotation ? (
243 <AnnotationCard annotation={item.annotation} />
244 ) : item.highlight ? (
245 <HighlightCard highlight={item.highlight} />
246 ) : item.bookmark ? (
247 <BookmarkCard bookmark={item.bookmark} />
248 ) : (
249 <div className="card" style={{ padding: "16px" }}>
250 <p className="text-secondary">Item could not be loaded</p>
251 </div>
252 )}
253 </div>
254 ))
255 )}
256 </div>
257
258 {isOwner && (
259 <CollectionModal
260 isOpen={isEditModalOpen}
261 onClose={() => setIsEditModalOpen(false)}
262 onSuccess={handleEditSuccess}
263 collectionToEdit={collection}
264 />
265 )}
266 </div>
267 );
268}