Skip to main content

max / makenotwork

16.4 KB · 359 lines History Blame Raw
1 <!-- Basic Information -->
2 <div id="item-details-tab" data-item-id="{{ item.id }}">
3 <div class="content-section">
4 <div class="section-header">
5 <h2 class="subsection-title">Basic Information</h2>
6 </div>
7
8 <div class="form-group">
9 <label for="item-name">Item Name</label>
10 <input type="text" id="item-name" value="{{ item.title }}">
11 </div>
12
13 <div class="form-group">
14 <label for="item-description">Description</label>
15 <textarea id="item-description">{{ item.description }}</textarea>
16 </div>
17
18 <div class="form-group">
19 <label>Item Image</label>
20 <div class="image-preview-row">
21 <div id="item-image-preview" class="image-preview-thumb">
22 {% match item.cover_image_url %}
23 {% when Some with (url) %}
24 <img src="{{ url }}" alt="Item image" class="square-cover">
25 {% when None %}
26 <span class="image-preview-empty">No image</span>
27 {% endmatch %}
28 </div>
29 <div>
30 <p class="text-sm muted mb-2 m-0">Square, at least 400x400px. JPG, PNG, or WebP.</p>
31 <input type="file" id="item-image-input" class="sr-only" accept="image/jpeg,image/png,image/webp">
32 <button type="button" class="btn-secondary btn-compact" onclick="document.getElementById('item-image-input').click()">Change Image</button>
33 <span id="item-image-status" class="ml-2 text-sm"></span>
34 </div>
35 </div>
36 </div>
37
38 <div class="form-group">
39 <label>Gallery Images</label>
40 <p class="text-sm muted mb-2 m-0">Up to 8 extra images shown in a carousel on the item page. The Item Image above stays the card/preview image.</p>
41 <div id="item-gallery-list" class="gallery-manager-list"></div>
42 <input type="file" id="item-gallery-input" class="sr-only" accept="image/jpeg,image/png,image/webp" multiple>
43 <button type="button" class="btn-secondary btn-compact" onclick="document.getElementById('item-gallery-input').click()">Add Images</button>
44 <span id="item-gallery-status" class="ml-2 text-sm"></span>
45 </div>
46
47 <div class="form-group">
48 <label for="item-type">Item Type</label>
49 <select id="item-type">
50 <option>Plugin</option>
51 <option>Sample Pack</option>
52 <option>Preset Pack</option>
53 <option>Course</option>
54 <option>Documentation</option>
55 <option>Template</option>
56 <option>Other</option>
57 </select>
58 </div>
59
60 <div class="form-group">
61 <label for="item-tags">Tags</label>
62 <div class="tag-input" id="tags-container">
63 {% for tag in item.tags %}
64 <span class="tag{% if tag.is_primary %} tag-primary{% endif %}">
65 {{ tag.name }}
66 {% if tag.is_primary %}<span class="tag-primary-dot" title="Primary tag"></span>{% endif %}
67 <button hx-delete="/api/items/{{ item.id }}/tags/{{ tag.id }}"
68 hx-target="closest .tag"
69 hx-swap="outerHTML"
70 hx-confirm="Remove this tag?">&times;</button>
71 {% if !tag.is_primary %}
72 <button class="tag-star" title="Set as primary"
73 hx-put="/api/items/{{ item.id }}/primary-tag"
74 hx-vals='{"tag_id": "{{ tag.id }}"}'
75 hx-swap="none"
76 hx-on::after-request="document.getElementById('tab-details').click()">&#9734;</button>
77 {% endif %}
78 </span>
79 {% endfor %}
80 </div>
81 <div id="tag-suggestions-auto"
82 hx-get="/api/items/{{ item.id }}/tag-suggestions"
83 hx-trigger="load"
84 hx-swap="innerHTML">
85 </div>
86 <div class="tag-search-wrap">
87 <input type="text" id="item-tags" placeholder="Search tags..."
88 autocomplete="off"
89 oninput="searchTags(this.value)">
90 <div id="tag-suggestions" class="tag-suggestions hidden"></div>
91 </div>
92 </div>
93
94 <details class="advanced-details">
95 <summary class="advanced-summary">Advanced</summary>
96 <div class="mt-4">
97 <div class="form-group">
98 <label for="release-date">Release Date</label>
99 <input type="date" id="release-date" value="{{ item.release_date }}">
100 </div>
101
102 <div class="form-group">
103 <label for="ai_tier">AI Classification <a href="/docs/ai" class="text-xs dimmed">What's this?</a></label>
104 <select id="ai_tier" name="ai_tier">
105 <option value="handmade" {% if item.ai_tier == "handmade" %}selected{% endif %}>Handmade: no AI tools used</option>
106 <option value="assisted" {% if item.ai_tier == "assisted" %}selected{% endif %}>AI-Assisted: AI tools with human creation</option>
107 <option value="generated" {% if item.ai_tier == "generated" %}selected{% endif %}>AI-Generated: primarily created by AI</option>
108 </select>
109 </div>
110 <div class="form-group{% if item.ai_tier != "assisted" %} hidden{% endif %}" id="ai-disclosure-row">
111 <label for="ai_disclosure">AI Disclosure</label>
112 <textarea id="ai_disclosure" name="ai_disclosure" rows="3" placeholder="Describe how AI tools were used...">{{ item.ai_disclosure.as_deref().unwrap_or_default() }}</textarea>
113 <small>Required for Assisted tier. Visible to fans before purchase.</small>
114 </div>
115 </div>
116 </details>
117
118 <form hx-put="/api/items/{{ item.id }}"
119 hx-target="#item-save-status"
120 hx-swap="innerHTML"
121 hx-indicator="#item-spinner"
122 class="mt-5">
123 <input type="hidden" name="title" id="hidden-title">
124 <input type="hidden" name="description" id="hidden-desc">
125 <input type="hidden" name="ai_tier" id="hidden-ai-tier">
126 <input type="hidden" name="ai_disclosure" id="hidden-ai-disclosure">
127 <button class="btn-primary" type="submit" onclick="document.getElementById('hidden-title').value=document.getElementById('item-name').value; document.getElementById('hidden-desc').value=document.getElementById('item-description').value; document.getElementById('hidden-ai-tier').value=document.getElementById('ai_tier').value; document.getElementById('hidden-ai-disclosure').value=document.getElementById('ai_disclosure').value;">
128 Save Changes
129 <span id="item-spinner" class="htmx-indicator"> ...</span>
130 </button>
131 <span id="item-save-status"></span>
132 </form>
133 </div>
134
135 {% match item.content %}
136 {% when crate::types::ItemContent::Text with { body, word_count, reading_time_minutes, .. } %}
137 <!-- Text Content Editor -->
138 <div class="content-section">
139 <div class="section-header">
140 <h2 class="subsection-title">Content</h2>
141 </div>
142 {% include "partials/item_text_editor.html" %}
143 </div>
144 {% when crate::types::ItemContent::Audio with { audio_s3_key, duration_seconds, .. } %}
145 <!-- Audio File Upload -->
146 <div class="content-section">
147 <div class="section-header">
148 <h2 class="subsection-title">Audio File</h2>
149 </div>
150 {% include "partials/item_audio_upload.html" %}
151 </div>
152 {% when crate::types::ItemContent::Video with { video_s3_key, .. } %}
153 <!-- Video File -->
154 <div class="content-section">
155 <div class="section-header">
156 <h2 class="subsection-title">Video File</h2>
157 </div>
158 <p>{% if video_s3_key.is_some() %}Video uploaded. Replace via the content wizard.{% else %}No video uploaded yet. Use the content wizard to upload.{% endif %}</p>
159 </div>
160 {% when crate::types::ItemContent::Other %}
161 {% endmatch %}
162
163 {% if item.item_type == "bundle" %}
164 <!-- Bundle Contents -->
165 <div class="content-section" id="bundle-section">
166 <div class="section-header">
167 <h2 class="subsection-title">Bundle Contents (<span id="bundle-count">{{ bundle_items.len() }}</span> items)</h2>
168 </div>
169
170 <table class="bundle-table" id="bundle-table">
171 <thead>
172 <tr class="bundle-table-head">
173 <th class="bundle-cell bundle-cell--first">Item</th>
174 <th class="bundle-cell">Description</th>
175 <th class="bundle-cell">File</th>
176 <th class="bundle-cell bundle-cell--actions"></th>
177 </tr>
178 </thead>
179 <tbody id="bundle-items-list">
180 {% for child in bundle_items %}
181 <tr class="bundle-row" data-child-id="{{ child.id }}">
182 <td class="bundle-cell bundle-cell--first"><a href="/dashboard/item/{{ child.id }}">{{ child.title }}</a></td>
183 <td class="bundle-cell bundle-cell--desc">{{ child.description }}</td>
184 <td class="bundle-cell bundle-cell--file">
185 <a href="/dashboard/item/{{ child.id }}" class="text-xs">Manage files</a>
186 </td>
187 <td class="bundle-cell">
188 <button type="button" class="btn-secondary bundle-remove-btn" data-child-id="{{ child.id }}">Remove</button>
189 </td>
190 </tr>
191 {% endfor %}
192 </tbody>
193 </table>
194
195 {% if bundle_items.is_empty() %}
196 <p id="bundle-empty" class="muted">No items in this bundle yet. Add a row below.</p>
197 {% endif %}
198
199 <div id="bundle-new-rows"></div>
200
201 <div class="bundle-add-row">
202 <button type="button" class="btn-secondary btn-compact" id="bundle-add-row-btn">Add Item</button>
203 <span id="bundle-status" class="text-sm"></span>
204 </div>
205
206 {% if !bundleable_items.is_empty() %}
207 <details class="bundle-existing-details">
208 <summary class="bundle-existing-summary">Add existing item</summary>
209 <div class="bundle-existing-row">
210 <select id="bundle-add-select" class="bundle-existing-select">
211 <option value="">Select an item...</option>
212 {% for avail in bundleable_items %}
213 <option value="{{ avail.id }}">{{ avail.title }} ({{ avail.item_type }})</option>
214 {% endfor %}
215 </select>
216 <button type="button" class="btn-secondary btn-compact" id="bundle-add-btn">Add</button>
217 </div>
218 </details>
219 {% endif %}
220 </div>
221
222 {% endif %}
223
224 <!-- Sections Management -->
225 <details class="content-section" id="sections-management"{% if !sections.is_empty() %} open{% endif %}>
226 <summary class="cursor-pointer">
227 <h2 class="inline">Sections (<span id="section-count">{{ sections.len() }}</span>)</h2>
228 </summary>
229 <p class="sections-intro">Add tabbed content blocks to your public item page: great for Features, Installation, Specs, or FAQ. Buyers see these as tabs below the description. Max 10.</p>
230
231 <div id="sections-list">
232 {% if sections.is_empty() %}
233 <p id="sections-empty" class="muted">No sections yet. Common sections: Features, Installation, Specs, FAQ, Changelog.</p>
234 {% else %}
235 {% for section in sections %}
236 <div class="section-mgmt-row" data-id="{{ section.id }}">
237 <span class="section-mgmt-title">{{ section.title }}</span>
238 <span class="text-xs dimmed">{{ section.body.chars().count() }} chars</span>
239 <button type="button" class="btn-secondary section-edit-btn" data-id="{{ section.id }}">Edit</button>
240 <button type="button" class="btn-secondary section-del-btn" data-id="{{ section.id }}">Delete</button>
241 </div>
242 {% endfor %}
243 {% endif %}
244 </div>
245
246 <details id="section-add-details" class="section-add-details">
247 <summary class="section-add-summary">Add Section</summary>
248 <div class="mt-3">
249 <div class="form-group">
250 <label for="new-sec-title">Title</label>
251 <input type="text" id="new-sec-title" placeholder="e.g. Features, Installation..." autocomplete="off">
252 </div>
253 <div class="form-group">
254 <label for="new-sec-body">Body (Markdown)</label>
255 <textarea id="new-sec-body" rows="6" placeholder="Section content..."></textarea>
256 <button type="button" class="btn-secondary insert-image-btn" onclick="mediaPickerOpen('new-sec-body')">Insert Image</button>
257 </div>
258 <button type="button" class="btn-secondary" id="add-sec-btn">Add Section</button>
259 <span id="sec-add-status" class="ml-2 text-sm"></span>
260 </div>
261 </details>
262
263 <!-- Edit modal (hidden, shown inline when editing) -->
264 <div id="section-edit-modal" class="section-edit-modal hidden">
265 <input type="hidden" id="edit-sec-id">
266 <div class="form-group">
267 <label for="edit-sec-title">Title</label>
268 <input type="text" id="edit-sec-title" autocomplete="off">
269 </div>
270 <div class="form-group">
271 <label for="edit-sec-body">Body (Markdown)</label>
272 <textarea id="edit-sec-body" rows="6"></textarea>
273 <button type="button" class="btn-secondary insert-image-btn" onclick="mediaPickerOpen('edit-sec-body')">Insert Image</button>
274 </div>
275 <div class="section-edit-actions">
276 <button type="button" class="btn-primary btn-compact" id="save-sec-btn">Save</button>
277 <button type="button" class="btn-secondary btn-compact" id="cancel-sec-btn">Cancel</button>
278 </div>
279 <span id="sec-edit-status" class="ml-2 text-sm"></span>
280 </div>
281 </details>
282 <!-- Publishing (merged from Settings) -->
283 <div class="content-section">
284 <div class="section-header">
285 <h2 class="subsection-title">Publishing</h2>
286 </div>
287
288 {% if let Some(scheduled) = item.publish_at %}
289 <p class="publish-note">
290 Scheduled for {{ scheduled }}.
291 </p>
292 <div class="action-buttons">
293 <button class="btn-secondary"
294 hx-put="/api/items/{{ item.id }}"
295 hx-vals='{"publish_at": ""}'
296 hx-confirm="Cancel the scheduled publish?"
297 hx-on::after-request="if(event.detail.successful) document.getElementById('tab-details').click()">Cancel Schedule</button>
298 </div>
299 {% else if item.is_public %}
300 <p class="publish-note">
301 Unpublishing removes this item from public view but preserves all data.
302 </p>
303 <div class="action-buttons">
304 <button class="btn-secondary"
305 hx-put="/api/items/{{ item.id }}"
306 hx-vals='{"is_public": "false"}'
307 hx-confirm="Unpublish this item? It will be hidden from public view."
308 hx-on::after-request="if(event.detail.successful) document.getElementById('tab-details').click()">Unpublish Item</button>
309 </div>
310 {% else %}
311 <p class="publish-note">
312 Publish this item to make it visible to the public.
313 </p>
314 <div class="action-buttons">
315 <button class="btn-primary"
316 hx-put="/api/items/{{ item.id }}"
317 hx-vals='{"is_public": "true"}'
318 hx-confirm="Publish this item?"
319 hx-on::after-request="if(event.detail.successful) document.getElementById('tab-details').click()">Publish Now</button>
320 <button class="btn-secondary" onclick="document.getElementById('schedule-form').classList.remove('hidden')">Schedule</button>
321 </div>
322 <form id="schedule-form" class="hidden mt-4"
323 hx-put="/api/items/{{ item.id }}"
324 hx-on:htmx:config-request="var v=event.detail.parameters.publish_at; if(v){ event.detail.parameters.publish_at=new Date(v).toISOString(); }"
325 hx-on::after-request="if(event.detail.successful) document.getElementById('tab-details').click()">
326 <div class="form-group">
327 <label for="publish-at">Publish at</label>
328 <input type="datetime-local" id="publish-at" name="publish_at" required>
329 <div class="hint">Uses your computer's time zone.</div>
330 </div>
331 <button type="submit" class="btn-primary">Schedule Publish</button>
332 </form>
333 {% endif %}
334 </div>
335
336 <script>
337 // Loaded via HTMX into #tab-content (no DOMContentLoaded), so init immediately,
338 // lazy-loading gallery.js if needed.
339 (function() {
340 function go() {
341 initGalleryManager({
342 targetType: 'item',
343 targetId: '{{ item.id }}',
344 listId: 'item-gallery-list',
345 inputId: 'item-gallery-input',
346 statusId: 'item-gallery-status'
347 });
348 }
349 if (window.initGalleryManager) { go(); return; }
350 var s = document.createElement('script');
351 s.src = '/static/gallery.js';
352 s.onload = go;
353 document.head.appendChild(s);
354 })();
355 </script>
356
357 </div>
358
359