Skip to main content

max / makenotwork

15.6 KB · 339 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 {# Rendered here rather than fetched on load, so a reader with no
82 script gets the suggestions too. The endpoint still serves this
83 partial for the refresh after a tag is added. #}
84 <div id="tag-suggestions-auto">{{ tag_suggestions|safe }}</div>
85 <div class="tag-search-wrap">
86 <input type="text" id="item-tags" placeholder="Search tags..."
87 autocomplete="off"
88 data-input="onSearchTagsInput">
89 <div id="tag-suggestions" class="tag-suggestions hidden"></div>
90 </div>
91 </div>
92
93 <details class="advanced-details">
94 <summary class="advanced-summary">Advanced</summary>
95 <div class="mt-section">
96 <div class="form-group">
97 <label for="release-date">Release Date</label>
98 <input type="date" id="release-date" value="{{ item.release_date }}">
99 </div>
100
101 <div class="form-group">
102 <label for="ai_tier">AI Classification <a href="/docs/generative-ai" class="text-xs dimmed">What's this?</a></label>
103 <select id="ai_tier" name="ai_tier">
104 <option value="handmade" {% if item.ai_tier == "handmade" %}selected{% endif %}>Handmade: no AI tools used</option>
105 <option value="assisted" {% if item.ai_tier == "assisted" %}selected{% endif %}>AI-Assisted: AI tools with human creation</option>
106 <option value="generated" {% if item.ai_tier == "generated" %}selected{% endif %}>AI-Generated: primarily created by AI</option>
107 </select>
108 </div>
109 <div class="form-group{% if item.ai_tier != "assisted" %} hidden{% endif %}" id="ai-disclosure-row">
110 <label for="ai_disclosure">AI Disclosure</label>
111 <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>
112 <small>Required for Assisted tier. Visible to fans before purchase.</small>
113 </div>
114 </div>
115 </details>
116
117 <form hx-put="/api/items/{{ item.id }}"
118 hx-target="#item-save-status"
119 hx-swap="innerHTML"
120 hx-indicator="#item-spinner"
121 class="mt-pane">
122 <input type="hidden" name="title" id="hidden-title">
123 <input type="hidden" name="description" id="hidden-desc">
124 <input type="hidden" name="ai_tier" id="hidden-ai-tier">
125 <input type="hidden" name="ai_disclosure" id="hidden-ai-disclosure">
126 <button class="btn-primary" type="submit" data-action="syncItemHiddenFields">
127 Save Changes
128 <span id="item-spinner" class="htmx-indicator"> ...</span>
129 </button>
130 <span id="item-save-status"></span>
131 </form>
132 </div>
133
134 {% match item.content %}
135 {% when crate::types::ItemContent::Text with { body, word_count, reading_time_minutes, .. } %}
136 <!-- Text Content Editor -->
137 <div class="content-section raised">
138 <div class="section-header">
139 <h2 class="subsection-title">Content</h2>
140 </div>
141 {% include "partials/item_text_editor.html" %}
142 </div>
143 {% when crate::types::ItemContent::Audio with { audio_s3_key, duration_seconds, .. } %}
144 <!-- Audio File Upload -->
145 <div class="content-section raised">
146 <div class="section-header">
147 <h2 class="subsection-title">Audio File</h2>
148 </div>
149 {% include "partials/item_audio_upload.html" %}
150 </div>
151 {% when crate::types::ItemContent::Video with { video_s3_key, .. } %}
152 <!-- Video File -->
153 <div class="content-section raised">
154 <div class="section-header">
155 <h2 class="subsection-title">Video File</h2>
156 </div>
157 <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>
158 </div>
159 {% when crate::types::ItemContent::Other %}
160 {% endmatch %}
161
162 {% if item.item_type == "bundle" %}
163 <!-- Bundle Contents -->
164 <div class="content-section raised" id="bundle-section">
165 <div class="section-header">
166 <h2 class="subsection-title">Bundle Contents (<span id="bundle-count">{{ bundle_items.len() }}</span> items)</h2>
167 </div>
168
169 <table class="bundle-table" id="bundle-table">
170 <thead>
171 <tr class="bundle-table-head">
172 <th class="bundle-cell bundle-cell--first">Item</th>
173 <th class="bundle-cell">Description</th>
174 <th class="bundle-cell">File</th>
175 <th class="bundle-cell bundle-cell--actions"></th>
176 </tr>
177 </thead>
178 <tbody id="bundle-items-list">
179 {% for child in bundle_items %}
180 <tr class="bundle-row" data-child-id="{{ child.id }}">
181 <td class="bundle-cell bundle-cell--first"><a href="/dashboard/item/{{ child.id }}">{{ child.title }}</a></td>
182 <td class="bundle-cell bundle-cell--desc">{{ child.description }}</td>
183 <td class="bundle-cell bundle-cell--file">
184 <a href="/dashboard/item/{{ child.id }}" class="text-xs">Manage files</a>
185 </td>
186 <td class="bundle-cell">
187 <button type="button" class="btn-secondary bundle-remove-btn" data-child-id="{{ child.id }}">Remove</button>
188 </td>
189 </tr>
190 {% endfor %}
191 </tbody>
192 </table>
193
194 {% if bundle_items.is_empty() %}
195 <p id="bundle-empty" class="muted">No items in this bundle yet. Add a row below.</p>
196 {% endif %}
197
198 <div id="bundle-new-rows"></div>
199
200 <div class="bundle-add-row">
201 <button type="button" class="btn-secondary btn-compact" id="bundle-add-row-btn">Add Item</button>
202 <span id="bundle-status" class="text-sm"></span>
203 </div>
204
205 {% if !bundleable_items.is_empty() %}
206 <details class="bundle-existing-details">
207 <summary class="bundle-existing-summary">Add existing item</summary>
208 <div class="bundle-existing-row">
209 <select id="bundle-add-select" class="bundle-existing-select">
210 <option value="">Select an item...</option>
211 {% for avail in bundleable_items %}
212 <option value="{{ avail.id }}">{{ avail.title }} ({{ avail.item_type }})</option>
213 {% endfor %}
214 </select>
215 <button type="button" class="btn-secondary btn-compact" id="bundle-add-btn">Add</button>
216 </div>
217 </details>
218 {% endif %}
219 </div>
220
221 {% endif %}
222
223 <!-- Sections Management -->
224 <details class="content-section raised" id="sections-management"{% if !sections.is_empty() %} open{% endif %}>
225 <summary class="cursor-pointer">
226 <h2 class="inline">Sections (<span id="section-count">{{ sections.len() }}</span>)</h2>
227 </summary>
228 <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>
229
230 <div id="sections-list">
231 {% if sections.is_empty() %}
232 <p id="sections-empty" class="muted">No sections yet. Common sections: Features, Installation, Specs, FAQ, Changelog.</p>
233 {% else %}
234 {% for section in sections %}
235 <div class="section-mgmt-row" data-id="{{ section.id }}">
236 <span class="section-mgmt-title">{{ section.title }}</span>
237 <span class="text-xs dimmed">{{ section.body.chars().count() }} chars</span>
238 <button type="button" class="btn-secondary section-edit-btn" data-id="{{ section.id }}">Edit</button>
239 <button type="button" class="btn-secondary section-del-btn" data-id="{{ section.id }}">Delete</button>
240 </div>
241 {% endfor %}
242 {% endif %}
243 </div>
244
245 <details id="section-add-details" class="section-add-details">
246 <summary class="section-add-summary">Add Section</summary>
247 <div class="mt-section">
248 <div class="form-group">
249 <label for="new-sec-title">Title</label>
250 <input type="text" id="new-sec-title" placeholder="e.g. Features, Installation..." autocomplete="off">
251 </div>
252 {# The described markdown editor, named rather than prefixed: three
253 editors share this document and a picker addresses its
254 destination by name, so `body` can only belong to one of them.
255 The id is the string `item-details.js` already reads. See
256 `crate::quasi::rich_field::named`. #}
257 {{ crate::quasi::rich_field::named(None, "new-sec-body", "Body (Markdown)", "Section content...", "", crate::quasi::rich_field::Height::Compact)|safe }}
258 {{ crate::quasi::media_picker::trigger("new-sec-body")|safe }}
259 <button type="button" class="btn-secondary" id="add-sec-btn">Add Section</button>
260 <span id="sec-add-status" class="ml-2 text-sm"></span>
261 </div>
262 </details>
263
264 <!-- Edit modal (hidden, shown inline when editing) -->
265 <div id="section-edit-modal" class="section-edit-modal hidden">
266 <input type="hidden" id="edit-sec-id">
267 <div class="form-group">
268 <label for="edit-sec-title">Title</label>
269 <input type="text" id="edit-sec-title" autocomplete="off">
270 </div>
271 {{ crate::quasi::rich_field::named(None, "edit-sec-body", "Body (Markdown)", "", "", crate::quasi::rich_field::Height::Compact)|safe }}
272 {{ crate::quasi::media_picker::trigger("edit-sec-body")|safe }}
273 <div class="section-edit-actions">
274 <button type="button" class="btn-primary btn-compact" id="save-sec-btn">Save</button>
275 <button type="button" class="btn-secondary btn-compact" id="cancel-sec-btn">Cancel</button>
276 </div>
277 <span id="sec-edit-status" class="ml-2 text-sm"></span>
278 </div>
279 </details>
280 <!-- Publishing (merged from Settings) -->
281 <div class="content-section raised">
282 <div class="section-header">
283 <h2 class="subsection-title">Publishing</h2>
284 </div>
285
286 {% if let Some(scheduled) = item.publish_at %}
287 <p class="publish-note">
288 Scheduled for {{ scheduled }}.
289 </p>
290 <div class="action-buttons">
291 <button class="btn-secondary"
292 hx-put="/api/items/{{ item.id }}"
293 hx-vals='{"publish_at": ""}'
294 hx-confirm="Cancel the scheduled publish?"
295 data-after="click" data-target="tab-details">Cancel Schedule</button>
296 </div>
297 {% else if item.is_public %}
298 <p class="publish-note">
299 Unpublishing removes this item from public view but preserves all data.
300 </p>
301 <div class="action-buttons">
302 <button class="btn-secondary"
303 hx-put="/api/items/{{ item.id }}"
304 hx-vals='{"is_public": "false"}'
305 hx-confirm="Unpublish this item? It will be hidden from public view."
306 data-after="click" data-target="tab-details">Unpublish Item</button>
307 </div>
308 {% else %}
309 <p class="publish-note">
310 Publish this item to make it visible to the public.
311 </p>
312 <div class="action-buttons">
313 <button class="btn-primary"
314 hx-put="/api/items/{{ item.id }}"
315 hx-vals='{"is_public": "true"}'
316 hx-confirm="Publish this item?"
317 data-after="click" data-target="tab-details">Publish Now</button>
318 <button class="btn-secondary" data-action="show" data-target="schedule-form">Schedule</button>
319 </div>
320 {# The field is described: `crate::quasi::schedule_field`. It carries the
321 label, the hint, that it is required, and that the value is submitted as
322 the moment it names rather than as the characters typed. That last was
323 `data-config="publish-at-iso"` and four lines of app JS; it is
324 makeover-layout's `Field::as_instant` now, and this was its last site. #}
325 <form id="schedule-form" class="hidden mt-section"
326 hx-put="/api/items/{{ item.id }}"
327 data-after="click" data-target="tab-details">
328 {{ crate::quasi::schedule_field::publish_at()|safe }}
329 <button type="submit" class="btn-primary">Schedule Publish</button>
330 </form>
331 {% endif %}
332 </div>
333
334 <div id="tab-item-details-cfg" hidden data-item-id="{{ item.id }}"></div>
335 <script src="/static/tab-item-details.js?v=0623"></script>
336
337 </div>
338
339