Skip to main content

max / makenotwork

15.5 KB · 340 lines History Blame Raw
1 <!-- Basic Information -->
2 <div id="item-details-tab" data-item-id="{{ item.id }}">
3 <div class="content-section raised">
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-peer 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" data-action="click" data-target="item-image-input">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-peer 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" data-action="click" data-target="item-gallery-input">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 data-after-always="click" data-target="tab-details">&#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 data-input="onSearchTagsInput">
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-section">
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/generative-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-pane">
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" data-action="syncItemHiddenFields">
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 raised">
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 raised">
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 raised">
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 raised" 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 raised" 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-section">
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 {# The described markdown editor, named rather than prefixed: three
254 editors share this document and a picker addresses its
255 destination by name, so `body` can only belong to one of them.
256 The id is the string `item-details.js` already reads. See
257 `crate::quasi::rich_field::named`. #}
258 {{ crate::quasi::rich_field::named(None, "new-sec-body", "Body (Markdown)", "Section content...", "", crate::quasi::rich_field::Height::Compact)|safe }}
259 {{ crate::quasi::media_picker::trigger("new-sec-body")|safe }}
260 <button type="button" class="btn-secondary" id="add-sec-btn">Add Section</button>
261 <span id="sec-add-status" class="ml-2 text-sm"></span>
262 </div>
263 </details>
264
265 <!-- Edit modal (hidden, shown inline when editing) -->
266 <div id="section-edit-modal" class="section-edit-modal hidden">
267 <input type="hidden" id="edit-sec-id">
268 <div class="form-group">
269 <label for="edit-sec-title">Title</label>
270 <input type="text" id="edit-sec-title" autocomplete="off">
271 </div>
272 {{ crate::quasi::rich_field::named(None, "edit-sec-body", "Body (Markdown)", "", "", crate::quasi::rich_field::Height::Compact)|safe }}
273 {{ crate::quasi::media_picker::trigger("edit-sec-body")|safe }}
274 <div class="section-edit-actions">
275 <button type="button" class="btn-primary btn-compact" id="save-sec-btn">Save</button>
276 <button type="button" class="btn-secondary btn-compact" id="cancel-sec-btn">Cancel</button>
277 </div>
278 <span id="sec-edit-status" class="ml-2 text-sm"></span>
279 </div>
280 </details>
281 <!-- Publishing (merged from Settings) -->
282 <div class="content-section raised">
283 <div class="section-header">
284 <h2 class="subsection-title">Publishing</h2>
285 </div>
286
287 {% if let Some(scheduled) = item.publish_at %}
288 <p class="publish-note">
289 Scheduled for {{ scheduled }}.
290 </p>
291 <div class="action-buttons">
292 <button class="btn-secondary"
293 hx-put="/api/items/{{ item.id }}"
294 hx-vals='{"publish_at": ""}'
295 hx-confirm="Cancel the scheduled publish?"
296 data-after="click" data-target="tab-details">Cancel Schedule</button>
297 </div>
298 {% else if item.is_public %}
299 <p class="publish-note">
300 Unpublishing removes this item from public view but preserves all data.
301 </p>
302 <div class="action-buttons">
303 <button class="btn-secondary"
304 hx-put="/api/items/{{ item.id }}"
305 hx-vals='{"is_public": "false"}'
306 hx-confirm="Unpublish this item? It will be hidden from public view."
307 data-after="click" data-target="tab-details">Unpublish Item</button>
308 </div>
309 {% else %}
310 <p class="publish-note">
311 Publish this item to make it visible to the public.
312 </p>
313 <div class="action-buttons">
314 <button class="btn-primary"
315 hx-put="/api/items/{{ item.id }}"
316 hx-vals='{"is_public": "true"}'
317 hx-confirm="Publish this item?"
318 data-after="click" data-target="tab-details">Publish Now</button>
319 <button class="btn-secondary" data-action="show" data-target="schedule-form">Schedule</button>
320 </div>
321 {# The field is described: `crate::quasi::schedule_field`. It carries the
322 label, the hint, that it is required, and that the value is submitted as
323 the moment it names rather than as the characters typed. That last was
324 `data-config="publish-at-iso"` and four lines of app JS; it is
325 makeover-layout's `Field::as_instant` now, and this was its last site. #}
326 <form id="schedule-form" class="hidden mt-section"
327 hx-put="/api/items/{{ item.id }}"
328 data-after="click" data-target="tab-details">
329 {{ crate::quasi::schedule_field::publish_at()|safe }}
330 <button type="submit" class="btn-primary">Schedule Publish</button>
331 </form>
332 {% endif %}
333 </div>
334
335 <div id="tab-item-details-cfg" hidden data-item-id="{{ item.id }}"></div>
336 <script src="/static/tab-item-details.js?v=0623"></script>
337
338 </div>
339
340