Explore the margin.at codebase, lexicons, and more! margin.at
at v0.1.11 8.3 kB view raw
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}