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
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, '"')}">
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, '<').replace(/>/g, '>')}</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);