1import { useState, useEffect, useCallback } from "react";
2import { X, Plus, Check, Folder } from "lucide-react";
3import {
4 getCollections,
5 addItemToCollection,
6 getCollectionsContaining,
7} from "../api/client";
8import { useAuth } from "../context/AuthContext";
9import CollectionModal from "./CollectionModal";
10
11export default function AddToCollectionModal({
12 isOpen,
13 onClose,
14 annotationUri,
15}) {
16 const { user } = useAuth();
17 const [collections, setCollections] = useState([]);
18 const [loading, setLoading] = useState(true);
19 const [addingTo, setAddingTo] = useState(null);
20 const [addedTo, setAddedTo] = useState(new Set());
21 const [createModalOpen, setCreateModalOpen] = useState(false);
22 const [error, setError] = useState(null);
23
24 useEffect(() => {
25 if (isOpen && user) {
26 if (!annotationUri) {
27 setLoading(false);
28 return;
29 }
30 loadCollections();
31 setError(null);
32 }
33 }, [isOpen, user, annotationUri, loadCollections]);
34
35 const loadCollections = useCallback(async () => {
36 try {
37 setLoading(true);
38 const [data, existingURIs] = await Promise.all([
39 getCollections(user?.did),
40 annotationUri ? getCollectionsContaining(annotationUri) : [],
41 ]);
42
43 const items = Array.isArray(data) ? data : data.items || [];
44 setCollections(items);
45 setAddedTo(new Set(existingURIs || []));
46 } catch (err) {
47 console.error(err);
48 setError("Failed to load collections");
49 } finally {
50 setLoading(false);
51 }
52 }, [user?.did, annotationUri]);
53
54 const handleAdd = async (collectionUri) => {
55 if (addedTo.has(collectionUri)) return;
56
57 try {
58 setAddingTo(collectionUri);
59 await addItemToCollection(collectionUri, annotationUri);
60 setAddedTo((prev) => new Set([...prev, collectionUri]));
61 } catch (err) {
62 console.error(err);
63 alert("Failed to add to collection");
64 } finally {
65 setAddingTo(null);
66 }
67 };
68
69 if (!isOpen) return null;
70
71 return (
72 <>
73 <div className="modal-overlay" onClick={onClose}>
74 <div
75 className="modal-container"
76 style={{
77 maxWidth: "380px",
78 maxHeight: "80dvh",
79 display: "flex",
80 flexDirection: "column",
81 }}
82 onClick={(e) => e.stopPropagation()}
83 >
84 <div className="modal-header">
85 <h2
86 className="modal-title"
87 style={{ display: "flex", alignItems: "center", gap: "8px" }}
88 >
89 <Folder size={20} style={{ color: "var(--accent)" }} />
90 Add to Collection
91 </h2>
92 <button onClick={onClose} className="modal-close-btn">
93 <X size={20} />
94 </button>
95 </div>
96
97 <div style={{ overflowY: "auto", padding: "8px", flex: 1 }}>
98 {loading ? (
99 <div
100 style={{
101 padding: "32px",
102 display: "flex",
103 alignItems: "center",
104 justifyContent: "center",
105 flexDirection: "column",
106 gap: "12px",
107 color: "var(--text-tertiary)",
108 }}
109 >
110 <div className="spinner"></div>
111 <span style={{ fontSize: "0.9rem" }}>
112 Loading collections...
113 </span>
114 </div>
115 ) : error ? (
116 <div style={{ padding: "24px", textAlign: "center" }}>
117 <p
118 className="text-error"
119 style={{ fontSize: "0.9rem", marginBottom: "12px" }}
120 >
121 {error}
122 </p>
123 <button
124 onClick={loadCollections}
125 className="btn btn-secondary btn-sm"
126 >
127 Try Again
128 </button>
129 </div>
130 ) : collections.length === 0 ? (
131 <div className="empty-state" style={{ padding: "32px" }}>
132 <div className="empty-state-icon">
133 <Folder size={24} />
134 </div>
135 <p className="empty-state-title" style={{ fontSize: "1rem" }}>
136 No collections found
137 </p>
138 <p className="empty-state-text">
139 Create a collection to start organizing your items.
140 </p>
141 </div>
142 ) : (
143 <div
144 style={{ display: "flex", flexDirection: "column", gap: "4px" }}
145 >
146 {collections.map((col) => {
147 const isAdded = addedTo.has(col.uri);
148 const isAdding = addingTo === col.uri;
149
150 return (
151 <button
152 key={col.uri}
153 onClick={() => handleAdd(col.uri)}
154 disabled={isAdding || isAdded}
155 className="collection-list-item"
156 style={{
157 opacity: isAdded ? 0.7 : 1,
158 cursor: isAdded ? "default" : "pointer",
159 }}
160 >
161 <div
162 style={{
163 display: "flex",
164 flexDirection: "column",
165 minWidth: 0,
166 }}
167 >
168 <span
169 style={{
170 fontWeight: 500,
171 overflow: "hidden",
172 textOverflow: "ellipsis",
173 whiteSpace: "nowrap",
174 }}
175 >
176 {col.name}
177 </span>
178 {col.description && (
179 <span
180 style={{
181 fontSize: "0.75rem",
182 color: "var(--text-tertiary)",
183 overflow: "hidden",
184 textOverflow: "ellipsis",
185 whiteSpace: "nowrap",
186 marginTop: "2px",
187 }}
188 >
189 {col.description}
190 </span>
191 )}
192 </div>
193
194 {isAdding ? (
195 <span
196 className="spinner spinner-sm"
197 style={{ marginLeft: "12px" }}
198 />
199 ) : isAdded ? (
200 <Check
201 size={20}
202 style={{
203 color: "var(--success)",
204 marginLeft: "12px",
205 }}
206 />
207 ) : (
208 <Plus
209 size={18}
210 style={{
211 color: "var(--text-tertiary)",
212 opacity: 0,
213 marginLeft: "12px",
214 }}
215 className="collection-list-item-icon"
216 />
217 )}
218 </button>
219 );
220 })}
221 </div>
222 )}
223 </div>
224
225 <div
226 style={{
227 padding: "16px",
228 borderTop: "1px solid var(--border)",
229 background: "var(--bg-tertiary)",
230 display: "flex",
231 gap: "8px",
232 }}
233 >
234 <button
235 onClick={() => setCreateModalOpen(true)}
236 className="btn btn-secondary"
237 style={{ flex: 1 }}
238 >
239 <Plus size={18} />
240 New Collection
241 </button>
242 <button
243 onClick={onClose}
244 className="btn btn-primary"
245 style={{ flex: 1 }}
246 >
247 Done
248 </button>
249 </div>
250 </div>
251 </div>
252
253 <CollectionModal
254 isOpen={createModalOpen}
255 onClose={() => setCreateModalOpen(false)}
256 onSuccess={() => {
257 loadCollections();
258 }}
259 />
260 </>
261 );
262}