Tend your corner of the atmosphere. spores.garden turns your AT Protocol records into a personal site with unique themes. Your data never leaves your PDS. Grow something that's truly yours. spores.garden
at main 257 lines 7.5 kB view raw
1/** 2 * Modal for creating and editing content blocks. 3 * Supports markdown content. 4 */ 5 6import { createRecord, putRecord, getCurrentDid } from '../oauth'; 7import { addSection, updateSection, getSiteOwnerDid } from '../config'; 8import { getCollection } from '../config/nsid'; 9import { setCachedActivity } from './recent-gardens'; 10 11class CreateContent extends HTMLElement { 12 private onClose: (() => void) | null = null; 13 private contentTitle: string = ''; 14 private contentContent: string = ''; 15 private editMode: boolean = false; 16 private editRkey: string | null = null; 17 private editSectionId: string | null = null; 18 19 connectedCallback() { 20 this.render(); 21 } 22 23 setOnClose(callback: () => void) { 24 this.onClose = callback; 25 } 26 27 show() { 28 this.style.display = 'flex'; 29 this.render(); 30 } 31 32 editContent(contentData: { 33 rkey?: string; 34 sectionId?: string; 35 title: string; 36 content: string; 37 format: string; 38 }) { 39 this.editMode = true; 40 this.editRkey = contentData.rkey || null; 41 this.editSectionId = contentData.sectionId || null; 42 this.contentTitle = contentData.title || ''; 43 this.contentContent = contentData.content || ''; 44 this.show(); 45 } 46 47 hide() { 48 this.style.display = 'none'; 49 } 50 51 private render() { 52 this.className = 'modal'; 53 this.style.display = 'flex'; 54 this.innerHTML = ` 55 <div class="modal-content create-content-modal"> 56 <h2>${this.editMode ? 'Edit Content' : 'Create Content'}</h2> 57 58 <div class="form-group"> 59 <label for="content-title">Title (optional)</label> 60 <input type="text" id="content-title" class="input" placeholder="Content title" maxlength="200" value="${(this.contentTitle || '').replace(/"/g, '&quot;')}"> 61 </div> 62 63 <div class="form-group"> 64 <label for="content-content">Content</label> 65 <textarea id="content-content" class="textarea" rows="10" placeholder="Enter your content here..." required>${(this.contentContent || '').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</textarea> 66 </div> 67 68 <div class="modal-actions"> 69 <button class="button button-primary" id="create-content-btn">${this.editMode ? 'Save Changes' : 'Create Content'}</button> 70 <button class="button button-secondary modal-close">Cancel</button> 71 </div> 72 </div> 73 `; 74 75 this.attachEventListeners(); 76 } 77 78 private attachEventListeners() { 79 const titleInput = this.querySelector('#content-title') as HTMLInputElement; 80 const contentTextarea = this.querySelector('#content-content') as HTMLTextAreaElement; 81 const createBtn = this.querySelector('#create-content-btn') as HTMLButtonElement; 82 const cancelBtn = this.querySelector('.modal-close') as HTMLButtonElement; 83 84 // Handle title input 85 titleInput?.addEventListener('input', (e) => { 86 this.contentTitle = (e.target as HTMLInputElement).value.trim(); 87 }); 88 89 // Handle content input 90 contentTextarea?.addEventListener('input', (e) => { 91 this.contentContent = (e.target as HTMLTextAreaElement).value; 92 }); 93 94 // Handle create/save button 95 createBtn?.addEventListener('click', async () => { 96 if (!this.contentContent.trim()) { 97 alert('Please enter some content.'); 98 return; 99 } 100 101 if (createBtn) { 102 createBtn.disabled = true; 103 createBtn.textContent = this.editMode ? 'Saving...' : 'Creating...'; 104 } 105 106 try { 107 if (this.editMode) { 108 await this.updateContentRecord({ 109 title: this.contentTitle || undefined, 110 content: this.contentContent 111 }); 112 } else { 113 await this.createContentRecord({ 114 title: this.contentTitle || undefined, 115 content: this.contentContent 116 }); 117 } 118 119 this.close(); 120 } catch (error) { 121 console.error(`Failed to ${this.editMode ? 'update' : 'create'} content:`, error); 122 alert(`Failed to ${this.editMode ? 'update' : 'create'} content: ${error instanceof Error ? error.message : 'Unknown error'}`); 123 if (createBtn) { 124 createBtn.disabled = false; 125 createBtn.textContent = this.editMode ? 'Save Changes' : 'Create Content'; 126 } 127 } 128 }); 129 130 // Handle cancel button 131 cancelBtn?.addEventListener('click', () => this.close()); 132 133 // Handle backdrop click 134 this.addEventListener('click', (e) => { 135 if (e.target === this) { 136 this.close(); 137 } 138 }); 139 } 140 141 private async createContentRecord(contentData: { 142 title?: string; 143 content: string; 144 }) { 145 const contentTextCollection = getCollection('contentText'); 146 // Create the content record 147 const record: any = { 148 $type: contentTextCollection, 149 content: contentData.content, 150 format: 'markdown', 151 createdAt: new Date().toISOString() 152 }; 153 154 if (contentData.title) { 155 record.title = contentData.title; 156 } 157 158 const response = await createRecord(contentTextCollection, record); 159 160 // Extract rkey from the response URI 161 const rkey = response.uri.split('/').pop(); 162 163 // Record local activity 164 const currentDid = getCurrentDid(); 165 if (currentDid) { 166 setCachedActivity(currentDid, 'edit', new Date()); 167 } 168 169 // Add section to config referencing this content 170 const section: any = { 171 type: 'content', 172 ref: response.uri, 173 rkey: rkey, 174 format: 'markdown' 175 }; 176 177 // Only add title if provided 178 if (contentData.title) { 179 section.title = contentData.title; 180 } 181 182 addSection(section); 183 184 // Trigger re-render 185 window.dispatchEvent(new CustomEvent('config-updated')); 186 } 187 188 private async updateContentRecord(contentData: { 189 title?: string; 190 content: string; 191 }) { 192 const ownerDid = getSiteOwnerDid(); 193 if (!ownerDid) { 194 throw new Error('Not logged in'); 195 } 196 197 const contentTextCollection = getCollection('contentText'); 198 if (this.editRkey) { 199 // Update existing content record 200 const record: any = { 201 $type: contentTextCollection, 202 content: contentData.content, 203 format: 'markdown', 204 createdAt: new Date().toISOString() 205 }; 206 207 if (contentData.title) { 208 record.title = contentData.title; 209 } 210 211 await putRecord(contentTextCollection, this.editRkey, record); 212 213 // Update section config if title changed 214 if (this.editSectionId) { 215 const updates: any = { format: 'markdown' }; 216 if (contentData.title) { 217 updates.title = contentData.title; 218 } 219 updateSection(this.editSectionId, updates); 220 } 221 } else if (this.editSectionId) { 222 // Update inline content section 223 const updates: any = { 224 content: contentData.content, 225 format: 'markdown' 226 }; 227 if (contentData.title) { 228 updates.title = contentData.title; 229 } 230 updateSection(this.editSectionId, updates); 231 } 232 233 // Record local activity 234 const currentDid = getCurrentDid(); 235 if (currentDid) { 236 setCachedActivity(currentDid, 'edit', new Date()); 237 } 238 239 // Trigger re-render 240 window.dispatchEvent(new CustomEvent('config-updated')); 241 } 242 243 private close() { 244 this.hide(); 245 if (this.onClose) { 246 this.onClose(); 247 } 248 // Reset form state 249 this.editMode = false; 250 this.editRkey = null; 251 this.editSectionId = null; 252 this.contentTitle = ''; 253 this.contentContent = ''; 254 } 255} 256 257customElements.define('create-content', CreateContent);