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.
10 kB · 396 lines
JSX
at ui-refactor
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397import { useState, useEffect } from "react";import { X, Folder, Star, Heart, Bookmark, Lightbulb, Zap, Coffee, Music, Camera, Code, Globe, Flag, Tag, Box, Archive, FileText, Image, Video, Mail, MapPin, Calendar, Clock, Search, Settings, User, Users, Home, Briefcase, Gift, Award, Target, TrendingUp, Activity, Cpu, Database, Cloud, Sun, Moon, Flame, Leaf, Trash2,} from "lucide-react";import { createCollection, updateCollection, deleteCollection,} from "../api/client";
const EMOJI_OPTIONS = [ "📁", "📚", "💡", "⭐", "🔖", "💻", "🎨", "📝", "🔬", "🎯", "🚀", "💎", "🌟", "📌", "💼", "🎮", "🎵", "🎬", "❤️", "🔥", "🌈", "🌸", "🌿", "🧠", "🏆", "📊", "🎓", "✨", "🔧", "⚡",];
const ICON_OPTIONS = [ { icon: Folder, name: "folder" }, { icon: Star, name: "star" }, { icon: Heart, name: "heart" }, { icon: Bookmark, name: "bookmark" }, { icon: Lightbulb, name: "lightbulb" }, { icon: Zap, name: "zap" }, { icon: Coffee, name: "coffee" }, { icon: Music, name: "music" }, { icon: Camera, name: "camera" }, { icon: Code, name: "code" }, { icon: Globe, name: "globe" }, { icon: Flag, name: "flag" }, { icon: Tag, name: "tag" }, { icon: Box, name: "box" }, { icon: Archive, name: "archive" }, { icon: FileText, name: "file" }, { icon: Image, name: "image" }, { icon: Video, name: "video" }, { icon: Mail, name: "mail" }, { icon: MapPin, name: "pin" }, { icon: Calendar, name: "calendar" }, { icon: Clock, name: "clock" }, { icon: Search, name: "search" }, { icon: Settings, name: "settings" }, { icon: User, name: "user" }, { icon: Users, name: "users" }, { icon: Home, name: "home" }, { icon: Briefcase, name: "briefcase" }, { icon: Gift, name: "gift" }, { icon: Award, name: "award" }, { icon: Target, name: "target" }, { icon: TrendingUp, name: "trending" }, { icon: Activity, name: "activity" }, { icon: Cpu, name: "cpu" }, { icon: Database, name: "database" }, { icon: Cloud, name: "cloud" }, { icon: Sun, name: "sun" }, { icon: Moon, name: "moon" }, { icon: Flame, name: "flame" }, { icon: Leaf, name: "leaf" },];
export default function CollectionModal({ isOpen, onClose, onSuccess, collectionToEdit, onDelete,}) { const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [icon, setIcon] = useState(""); const [customEmoji, setCustomEmoji] = useState(""); const [activeTab, setActiveTab] = useState("emoji"); const [loading, setLoading] = useState(false); const [deleting, setDeleting] = useState(false); const [error, setError] = useState(null);
useEffect(() => { if (collectionToEdit) { setName(collectionToEdit.name); setDescription(collectionToEdit.description || ""); const savedIcon = collectionToEdit.icon || ""; setIcon(savedIcon); setCustomEmoji(savedIcon);
if (savedIcon.startsWith("icon:")) { setActiveTab("icons"); } } else { setName(""); setDescription(""); setIcon(""); setCustomEmoji(""); } setError(null); }, [collectionToEdit, isOpen]);
if (!isOpen) return null;
const handleEmojiSelect = (emoji) => { if (icon === emoji) { setIcon(""); setCustomEmoji(""); } else { setIcon(emoji); setCustomEmoji(emoji); } };
const handleIconSelect = (iconName) => { const value = `icon:${iconName}`; if (icon === value) { setIcon(""); setCustomEmoji(""); } else { setIcon(value); setCustomEmoji(value); } };
const handleCustomEmojiChange = (e) => { const value = e.target.value; setCustomEmoji(value); const emojiMatch = value.match( /(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)/gu, ); if (emojiMatch && emojiMatch.length > 0) { setIcon(emojiMatch[emojiMatch.length - 1]); } else if (value === "") { setIcon(""); } };
const handleSubmit = async (e) => { e.preventDefault(); setLoading(true); setError(null);
try { if (collectionToEdit) { await updateCollection(collectionToEdit.uri, name, description, icon); } else { await createCollection(name, description, icon); } onSuccess(); onClose(); } catch (err) { console.error(err); setError(err.message || "Failed to save collection"); } finally { setLoading(false); } };
const handleDelete = async () => { if ( !confirm( "Delete this collection and all its items? This cannot be undone.", ) ) { return; } setDeleting(true); setError(null);
try { await deleteCollection(collectionToEdit.uri); if (onDelete) { onDelete(); } else { onSuccess(); } onClose(); } catch (err) { console.error(err); setError(err.message || "Failed to delete collection"); } finally { setDeleting(false); } };
return ( <div className="modal-overlay" onClick={onClose}> <div className="modal-container" style={{ maxWidth: "420px" }} onClick={(e) => e.stopPropagation()} > <div className="modal-header"> <h2 className="modal-title"> {collectionToEdit ? "Edit Collection" : "New Collection"} </h2> <button onClick={onClose} className="modal-close-btn"> <X size={20} /> </button> </div>
<form onSubmit={handleSubmit} className="modal-form"> {error && ( <div className="card text-error" style={{ padding: "12px", background: "rgba(239, 68, 68, 0.1)", borderColor: "rgba(239, 68, 68, 0.2)", fontSize: "0.9rem", }} > {error} </div> )}
<div className="form-group"> <label className="form-label">Icon</label> <div className="icon-picker-tabs"> <button type="button" className={`icon-picker-tab ${activeTab === "emoji" ? "active" : ""}`} onClick={() => setActiveTab("emoji")} > Emoji </button> <button type="button" className={`icon-picker-tab ${activeTab === "icons" ? "active" : ""}`} onClick={() => setActiveTab("icons")} > Icons </button> </div>
{activeTab === "emoji" && ( <div className="emoji-picker-wrapper"> <div className="emoji-custom-input"> <input type="text" value={customEmoji.startsWith("icon:") ? "" : customEmoji} onChange={handleCustomEmojiChange} placeholder="Type any emoji..." className="form-input" /> </div> <div className="emoji-picker"> {EMOJI_OPTIONS.map((emoji) => ( <button key={emoji} type="button" className={`emoji-option ${icon === emoji ? "selected" : ""}`} onClick={() => handleEmojiSelect(emoji)} > {emoji} </button> ))} </div> </div> )}
{activeTab === "icons" && ( <div className="icon-picker"> {ICON_OPTIONS.map(({ icon: IconComponent, name: iconName }) => ( <button key={iconName} type="button" className={`icon-option ${icon === `icon:${iconName}` ? "selected" : ""}`} onClick={() => handleIconSelect(iconName)} > <IconComponent size={20} /> </button> ))} </div> )} </div>
<div className="form-group"> <label className="form-label">Name</label> <input type="text" value={name} onChange={(e) => setName(e.target.value)} required className="form-input" placeholder="My Favorites" /> </div>
<div className="form-group"> <label className="form-label">Description</label> <textarea value={description} onChange={(e) => setDescription(e.target.value)} rows={2} className="form-textarea" placeholder="A collection of..." /> </div>
<div className="modal-actions"> {collectionToEdit && ( <button type="button" onClick={handleDelete} disabled={deleting} className="btn btn-danger" > <Trash2 size={16} /> {deleting ? "Deleting..." : "Delete"} </button> )} <div style={{ flex: 1 }} /> <button type="button" onClick={onClose} className="btn btn-ghost"> Cancel </button> <button type="submit" disabled={loading} className="btn btn-primary" style={loading ? { opacity: 0.7, cursor: "wait" } : {}} > {loading ? "Saving..." : collectionToEdit ? "Save Changes" : "Create Collection"} </button> </div> </form> </div> </div> );}