Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
at main 253 lines 10 kB view raw
1import React, { useState, useEffect } from "react"; 2import { X, Loader2 } from "lucide-react"; 3import CollectionIcon from "../common/CollectionIcon"; 4import { ICON_MAP } from "../common/iconMap"; 5import EmojiPicker, { Theme } from "emoji-picker-react"; 6import { updateCollection, type Collection } from "../../api/client"; 7import { useStore } from "@nanostores/react"; 8import { $theme } from "../../store/theme"; 9 10interface EditCollectionModalProps { 11 isOpen: boolean; 12 onClose: () => void; 13 collection: Collection; 14 onUpdate: (updatedCollection: Collection) => void; 15} 16 17export default function EditCollectionModal({ 18 isOpen, 19 onClose, 20 collection, 21 onUpdate, 22}: EditCollectionModalProps) { 23 const [name, setName] = useState(collection.name); 24 const [description, setDescription] = useState(collection.description || ""); 25 const initialIsIcon = collection.icon?.startsWith("icon:") ?? false; 26 const initialIconValue = collection.icon?.replace("icon:", "") || ""; 27 28 const [activeTab, setActiveTab] = useState<"icon" | "emoji">( 29 initialIsIcon || !collection.icon ? "icon" : "emoji", 30 ); 31 const [icon, setIcon] = useState(initialIconValue); 32 const [loading, setLoading] = useState(false); 33 const [error, setError] = useState<string | null>(null); 34 const theme = useStore($theme); 35 36 useEffect(() => { 37 if (isOpen) { 38 setName(collection.name); 39 setDescription(collection.description || ""); 40 41 const isIcon = collection.icon?.startsWith("icon:") ?? false; 42 setActiveTab(isIcon || !collection.icon ? "icon" : "emoji"); 43 setIcon(collection.icon?.replace("icon:", "") || ""); 44 45 setError(null); 46 document.body.style.overflow = "hidden"; 47 } 48 return () => { 49 document.body.style.overflow = "unset"; 50 }; 51 }, [isOpen, collection]); 52 53 const handleSubmit = async (e: React.FormEvent) => { 54 e.preventDefault(); 55 if (!name.trim()) return; 56 57 try { 58 setLoading(true); 59 setError(null); 60 const iconValue = icon 61 ? ICON_MAP[icon] 62 ? `icon:${icon}` 63 : icon 64 : undefined; 65 const updated = await updateCollection( 66 collection.uri, 67 name.trim(), 68 description.trim() || undefined, 69 iconValue, 70 ); 71 72 if (updated) { 73 onUpdate(updated); 74 onClose(); 75 } else { 76 setError("Failed to update collection"); 77 } 78 } catch (err) { 79 console.error(err); 80 setError("An error occurred while updating"); 81 } finally { 82 setLoading(false); 83 } 84 }; 85 86 if (!isOpen) return null; 87 88 return ( 89 <div 90 className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in" 91 onClick={onClose} 92 > 93 <div 94 className="w-full max-w-md bg-white dark:bg-surface-900 rounded-3xl shadow-2xl overflow-hidden" 95 onClick={(e) => e.stopPropagation()} 96 > 97 <div className="p-4 flex justify-between items-center border-b border-surface-100 dark:border-surface-800"> 98 <h2 className="text-xl font-display font-bold text-surface-900 dark:text-white"> 99 Edit Collection 100 </h2> 101 <button 102 onClick={onClose} 103 className="p-2 text-surface-400 hover:text-surface-900 dark:hover:text-white hover:bg-surface-50 dark:hover:bg-surface-800 rounded-full transition-colors" 104 > 105 <X size={20} /> 106 </button> 107 </div> 108 109 <div className="p-6"> 110 <form onSubmit={handleSubmit} className="space-y-4"> 111 <div> 112 <label className="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1"> 113 Collection name 114 </label> 115 <input 116 type="text" 117 className="w-full px-4 py-3 bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 rounded-xl focus:border-primary-500 dark:focus:border-primary-400 focus:ring-4 focus:ring-primary-500/10 outline-none transition-all text-surface-900 dark:text-white placeholder-surface-400 dark:placeholder-surface-500" 118 value={name} 119 onChange={(e) => setName(e.target.value)} 120 placeholder="My Collection" 121 autoFocus 122 /> 123 </div> 124 125 <div> 126 <label className="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-1"> 127 Description (optional) 128 </label> 129 <textarea 130 className="w-full px-4 py-3 bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 rounded-xl focus:border-primary-500 dark:focus:border-primary-400 focus:ring-4 focus:ring-primary-500/10 outline-none transition-all text-surface-900 dark:text-white placeholder-surface-400 dark:placeholder-surface-500 resize-none" 131 value={description} 132 onChange={(e) => setDescription(e.target.value)} 133 placeholder="What's this collection about?" 134 rows={3} 135 /> 136 </div> 137 138 <div> 139 <label className="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-2"> 140 Icon 141 </label> 142 143 <div className="flex gap-2 mb-3 bg-surface-100 dark:bg-surface-800 p-1 rounded-xl"> 144 <button 145 type="button" 146 onClick={() => setActiveTab("icon")} 147 className={`flex-1 py-1.5 text-sm font-medium rounded-lg transition-colors ${ 148 activeTab === "icon" 149 ? "bg-white dark:bg-surface-700 text-surface-900 dark:text-white shadow-sm" 150 : "text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-200" 151 }`} 152 > 153 Icons 154 </button> 155 <button 156 type="button" 157 onClick={() => setActiveTab("emoji")} 158 className={`flex-1 py-1.5 text-sm font-medium rounded-lg transition-colors ${ 159 activeTab === "emoji" 160 ? "bg-white dark:bg-surface-700 text-surface-900 dark:text-white shadow-sm" 161 : "text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-200" 162 }`} 163 > 164 Emojis 165 </button> 166 </div> 167 168 {activeTab === "icon" ? ( 169 <div className="grid grid-cols-8 gap-1.5 max-h-60 overflow-y-auto p-2 bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 custom-scrollbar"> 170 {Object.keys(ICON_MAP).map((iconName) => { 171 const isSelected = icon === iconName; 172 return ( 173 <button 174 key={iconName} 175 type="button" 176 onClick={() => setIcon(isSelected ? "" : iconName)} 177 className={`w-8 h-8 flex items-center justify-center rounded-lg transition-all ${ 178 isSelected 179 ? "bg-primary-600 text-white" 180 : "hover:bg-surface-200 dark:hover:bg-surface-700 text-surface-600 dark:text-surface-400" 181 }`} 182 title={iconName} 183 > 184 <CollectionIcon icon={`icon:${iconName}`} size={16} /> 185 </button> 186 ); 187 })} 188 </div> 189 ) : ( 190 <div className="w-full bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 overflow-hidden"> 191 <EmojiPicker 192 className="custom-emoji-picker" 193 onEmojiClick={(emojiData) => setIcon(emojiData.emoji)} 194 autoFocusSearch={false} 195 width="100%" 196 height={300} 197 previewConfig={{ showPreview: false }} 198 skinTonesDisabled 199 lazyLoadEmojis 200 theme={ 201 theme === "dark" || 202 (theme === "system" && 203 window.matchMedia("(prefers-color-scheme: dark)") 204 .matches) 205 ? (Theme.DARK as Theme) 206 : (Theme.LIGHT as Theme) 207 } 208 /> 209 </div> 210 )} 211 212 {icon && ( 213 <p className="mt-2 text-sm text-surface-600 dark:text-surface-300 flex items-center gap-2"> 214 Selected: 215 <span className="inline-flex items-center justify-center w-8 h-8 bg-surface-100 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700"> 216 <CollectionIcon 217 icon={ICON_MAP[icon] ? `icon:${icon}` : icon} 218 size={18} 219 /> 220 </span> 221 </p> 222 )} 223 </div> 224 225 {error && ( 226 <div className="p-3 bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400 text-sm rounded-lg"> 227 {error} 228 </div> 229 )} 230 231 <div className="flex gap-3 pt-2"> 232 <button 233 type="button" 234 className="flex-1 py-3 bg-white dark:bg-surface-800 border border-surface-200 dark:border-surface-700 text-surface-700 dark:text-surface-200 font-semibold rounded-xl hover:bg-surface-50 dark:hover:bg-surface-700 transition-colors" 235 onClick={onClose} 236 > 237 Cancel 238 </button> 239 <button 240 type="submit" 241 className="flex-1 py-3 bg-primary-600 text-white font-semibold rounded-xl hover:bg-primary-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-2" 242 disabled={!name.trim() || loading} 243 > 244 {loading && <Loader2 size={16} className="animate-spin" />} 245 {loading ? "Saving..." : "Save Changes"} 246 </button> 247 </div> 248 </form> 249 </div> 250 </div> 251 </div> 252 ); 253}