Something went wrong. Try again.
(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol.
margin.at
extension web atproto comments
Something went wrong. Try again.
8.4 kB · 262 lines
JSX
at ui-refactor
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263import { useState, useEffect, useCallback } from "react";import { X, Plus, Check, Folder } from "lucide-react";import { getCollections, addItemToCollection, getCollectionsContaining,} from "../api/client";import { useAuth } from "../context/AuthContext";import CollectionModal from "./CollectionModal";
export default function AddToCollectionModal({ isOpen, onClose, annotationUri,}) { const { user } = useAuth(); const [collections, setCollections] = useState([]); const [loading, setLoading] = useState(true); const [addingTo, setAddingTo] = useState(null); const [addedTo, setAddedTo] = useState(new Set()); const [createModalOpen, setCreateModalOpen] = useState(false); const [error, setError] = useState(null);
const loadCollections = useCallback(async () => { try { setLoading(true); const [data, existingURIs] = await Promise.all([ getCollections(user?.did), annotationUri ? getCollectionsContaining(annotationUri) : [], ]);
const items = Array.isArray(data) ? data : data.items || []; setCollections(items); setAddedTo(new Set(existingURIs || [])); } catch (err) { console.error(err); setError("Failed to load collections"); } finally { setLoading(false); } }, [user?.did, annotationUri]);
useEffect(() => { if (isOpen && user) { if (!annotationUri) { setLoading(false); return; } loadCollections(); setError(null); } }, [isOpen, user, annotationUri, loadCollections]);
const handleAdd = async (collectionUri) => { if (addedTo.has(collectionUri)) return;
try { setAddingTo(collectionUri); await addItemToCollection(collectionUri, annotationUri); setAddedTo((prev) => new Set([...prev, collectionUri])); } catch (err) { console.error(err); alert("Failed to add to collection"); } finally { setAddingTo(null); } };
if (!isOpen) return null;
return ( <> <div className="modal-overlay" onClick={onClose}> <div className="modal-container" style={{ maxWidth: "380px", maxHeight: "80dvh", display: "flex", flexDirection: "column", }} onClick={(e) => e.stopPropagation()} > <div className="modal-header"> <h2 className="modal-title" style={{ display: "flex", alignItems: "center", gap: "8px" }} > <Folder size={20} style={{ color: "var(--accent)" }} /> Add to Collection </h2> <button onClick={onClose} className="modal-close-btn"> <X size={20} /> </button> </div>
<div style={{ overflowY: "auto", padding: "8px", flex: 1 }}> {loading ? ( <div style={{ padding: "32px", display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column", gap: "12px", color: "var(--text-tertiary)", }} > <div className="spinner"></div> <span style={{ fontSize: "0.9rem" }}> Loading collections... </span> </div> ) : error ? ( <div style={{ padding: "24px", textAlign: "center" }}> <p className="text-error" style={{ fontSize: "0.9rem", marginBottom: "12px" }} > {error} </p> <button onClick={loadCollections} className="btn btn-secondary btn-sm" > Try Again </button> </div> ) : collections.length === 0 ? ( <div className="empty-state" style={{ padding: "32px" }}> <div className="empty-state-icon"> <Folder size={24} /> </div> <p className="empty-state-title" style={{ fontSize: "1rem" }}> No collections found </p> <p className="empty-state-text"> Create a collection to start organizing your items. </p> </div> ) : ( <div style={{ display: "flex", flexDirection: "column", gap: "4px" }} > {collections.map((col) => { const isAdded = addedTo.has(col.uri); const isAdding = addingTo === col.uri;
return ( <button key={col.uri} onClick={() => handleAdd(col.uri)} disabled={isAdding || isAdded} className="collection-list-item" style={{ opacity: isAdded ? 0.7 : 1, cursor: isAdded ? "default" : "pointer", }} > <div style={{ display: "flex", flexDirection: "column", minWidth: 0, }} > <span style={{ fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", }} > {col.name} </span> {col.description && ( <span style={{ fontSize: "0.75rem", color: "var(--text-tertiary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", marginTop: "2px", }} > {col.description} </span> )} </div>
{isAdding ? ( <span className="spinner spinner-sm" style={{ marginLeft: "12px" }} /> ) : isAdded ? ( <Check size={20} style={{ color: "var(--success)", marginLeft: "12px", }} /> ) : ( <Plus size={18} style={{ color: "var(--text-tertiary)", opacity: 0, marginLeft: "12px", }} className="collection-list-item-icon" /> )} </button> ); })} </div> )} </div>
<div style={{ padding: "16px", borderTop: "1px solid var(--border)", background: "var(--bg-tertiary)", display: "flex", gap: "8px", }} > <button onClick={() => setCreateModalOpen(true)} className="btn btn-secondary" style={{ flex: 1 }} > <Plus size={18} /> New Collection </button> <button onClick={onClose} className="btn btn-primary" style={{ flex: 1 }} > Done </button> </div> </div> </div>
<CollectionModal isOpen={createModalOpen} onClose={() => setCreateModalOpen(false)} onSuccess={() => { loadCollections(); }} /> </> );}