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