Skip to main content

max / makenotwork

15.1 KB · 328 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 for="item-type">Item Type</label>
40 <select id="item-type">
41 <option>Plugin</option>
42 <option>Sample Pack</option>
43 <option>Preset Pack</option>
44 <option>Course</option>
45 <option>Documentation</option>
46 <option>Template</option>
47 <option>Other</option>
48 </select>
49 </div>
50
51 <div class="form-group">
52 <label for="item-tags">Tags</label>
53 <div class="tag-input" id="tags-container">
54 {% for tag in item.tags %}
55 <span class="tag{% if tag.is_primary %} tag-primary{% endif %}">
56 {{ tag.name }}
57 {% if tag.is_primary %}<span class="tag-primary-dot" title="Primary tag"></span>{% endif %}
58 <button hx-delete="/api/items/{{ item.id }}/tags/{{ tag.id }}"
59 hx-target="closest .tag"
60 hx-swap="outerHTML"
61 hx-confirm="Remove this tag?">&times;</button>
62 {% if !tag.is_primary %}
63 <button class="tag-star" title="Set as primary"
64 hx-put="/api/items/{{ item.id }}/primary-tag"
65 hx-vals='{"tag_id": "{{ tag.id }}"}'
66 hx-swap="none"
67 hx-on::after-request="document.getElementById('tab-details').click()">&#9734;</button>
68 {% endif %}
69 </span>
70 {% endfor %}
71 </div>
72 <div id="tag-suggestions-auto"
73 hx-get="/api/items/{{ item.id }}/tag-suggestions"
74 hx-trigger="load"
75 hx-swap="innerHTML">
76 </div>
77 <div class="tag-search-wrap">
78 <input type="text" id="item-tags" placeholder="Search tags..."
79 autocomplete="off"
80 oninput="searchTags(this.value)">
81 <div id="tag-suggestions" class="tag-suggestions hidden"></div>
82 </div>
83 </div>
84
85 <details class="advanced-details">
86 <summary class="advanced-summary">Advanced</summary>
87 <div class="mt-4">
88 <div class="form-group">
89 <label for="release-date">Release Date</label>
90 <input type="date" id="release-date" value="{{ item.release_date }}">
91 </div>
92
93 <div class="form-group">
94 <label for="ai_tier">AI Classification <a href="/docs/ai" class="text-xs dimmed">What's this?</a></label>
95 <select id="ai_tier" name="ai_tier">
96 <option value="handmade" {% if item.ai_tier == "handmade" %}selected{% endif %}>Handmade: no AI tools used</option>
97 <option value="assisted" {% if item.ai_tier == "assisted" %}selected{% endif %}>AI-Assisted: AI tools with human creation</option>
98 <option value="generated" {% if item.ai_tier == "generated" %}selected{% endif %}>AI-Generated: primarily created by AI</option>
99 </select>
100 </div>
101 <div class="form-group{% if item.ai_tier != "assisted" %} hidden{% endif %}" id="ai-disclosure-row">
102 <label for="ai_disclosure">AI Disclosure</label>
103 <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>
104 <small>Required for Assisted tier. Visible to fans before purchase.</small>
105 </div>
106 </div>
107 </details>
108
109 <form hx-put="/api/items/{{ item.id }}"
110 hx-target="#item-save-status"
111 hx-swap="innerHTML"
112 hx-indicator="#item-spinner"
113 class="mt-5">
114 <input type="hidden" name="title" id="hidden-title">
115 <input type="hidden" name="description" id="hidden-desc">
116 <input type="hidden" name="ai_tier" id="hidden-ai-tier">
117 <input type="hidden" name="ai_disclosure" id="hidden-ai-disclosure">
118 <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;">
119 Save Changes
120 <span id="item-spinner" class="htmx-indicator"> ...</span>
121 </button>
122 <span id="item-save-status"></span>
123 </form>
124 </div>
125
126 {% match item.content %}
127 {% when crate::types::ItemContent::Text with { body, word_count, reading_time_minutes, .. } %}
128 <!-- Text Content Editor -->
129 <div class="content-section">
130 <div class="section-header">
131 <h2 class="subsection-title">Content</h2>
132 </div>
133 {% include "partials/item_text_editor.html" %}
134 </div>
135 {% when crate::types::ItemContent::Audio with { audio_s3_key, duration_seconds, .. } %}
136 <!-- Audio File Upload -->
137 <div class="content-section">
138 <div class="section-header">
139 <h2 class="subsection-title">Audio File</h2>
140 </div>
141 {% include "partials/item_audio_upload.html" %}
142 </div>
143 {% when crate::types::ItemContent::Video with { video_s3_key, .. } %}
144 <!-- Video File -->
145 <div class="content-section">
146 <div class="section-header">
147 <h2 class="subsection-title">Video File</h2>
148 </div>
149 <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>
150 </div>
151 {% when crate::types::ItemContent::Other %}
152 {% endmatch %}
153
154 {% if item.item_type == "bundle" %}
155 <!-- Bundle Contents -->
156 <div class="content-section" id="bundle-section">
157 <div class="section-header">
158 <h2 class="subsection-title">Bundle Contents (<span id="bundle-count">{{ bundle_items.len() }}</span> items)</h2>
159 </div>
160
161 <table class="bundle-table" id="bundle-table">
162 <thead>
163 <tr class="bundle-table-head">
164 <th class="bundle-cell bundle-cell--first">Item</th>
165 <th class="bundle-cell">Description</th>
166 <th class="bundle-cell">File</th>
167 <th class="bundle-cell bundle-cell--actions"></th>
168 </tr>
169 </thead>
170 <tbody id="bundle-items-list">
171 {% for child in bundle_items %}
172 <tr class="bundle-row" data-child-id="{{ child.id }}">
173 <td class="bundle-cell bundle-cell--first"><a href="/dashboard/item/{{ child.id }}">{{ child.title }}</a></td>
174 <td class="bundle-cell bundle-cell--desc">{{ child.description }}</td>
175 <td class="bundle-cell bundle-cell--file">
176 <a href="/dashboard/item/{{ child.id }}" class="text-xs">Manage files</a>
177 </td>
178 <td class="bundle-cell">
179 <button type="button" class="btn-secondary bundle-remove-btn" data-child-id="{{ child.id }}">Remove</button>
180 </td>
181 </tr>
182 {% endfor %}
183 </tbody>
184 </table>
185
186 {% if bundle_items.is_empty() %}
187 <p id="bundle-empty" class="muted">No items in this bundle yet. Add a row below.</p>
188 {% endif %}
189
190 <div id="bundle-new-rows"></div>
191
192 <div class="bundle-add-row">
193 <button type="button" class="btn-secondary btn-compact" id="bundle-add-row-btn">Add Item</button>
194 <span id="bundle-status" class="text-sm"></span>
195 </div>
196
197 {% if !bundleable_items.is_empty() %}
198 <details class="bundle-existing-details">
199 <summary class="bundle-existing-summary">Add existing item</summary>
200 <div class="bundle-existing-row">
201 <select id="bundle-add-select" class="bundle-existing-select">
202 <option value="">Select an item...</option>
203 {% for avail in bundleable_items %}
204 <option value="{{ avail.id }}">{{ avail.title }} ({{ avail.item_type }})</option>
205 {% endfor %}
206 </select>
207 <button type="button" class="btn-secondary btn-compact" id="bundle-add-btn">Add</button>
208 </div>
209 </details>
210 {% endif %}
211 </div>
212
213 {% endif %}
214
215 <!-- Sections Management -->
216 <details class="content-section" id="sections-management"{% if !sections.is_empty() %} open{% endif %}>
217 <summary class="cursor-pointer">
218 <h2 class="inline">Sections (<span id="section-count">{{ sections.len() }}</span>)</h2>
219 </summary>
220 <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>
221
222 <div id="sections-list">
223 {% if sections.is_empty() %}
224 <p id="sections-empty" class="muted">No sections yet. Common sections: Features, Installation, Specs, FAQ, Changelog.</p>
225 {% else %}
226 {% for section in sections %}
227 <div class="section-mgmt-row" data-id="{{ section.id }}">
228 <span class="section-mgmt-title">{{ section.title }}</span>
229 <span class="text-xs dimmed">{{ section.body.chars().count() }} chars</span>
230 <button type="button" class="btn-secondary section-edit-btn" data-id="{{ section.id }}">Edit</button>
231 <button type="button" class="btn-secondary section-del-btn" data-id="{{ section.id }}">Delete</button>
232 </div>
233 {% endfor %}
234 {% endif %}
235 </div>
236
237 <details id="section-add-details" class="section-add-details">
238 <summary class="section-add-summary">Add Section</summary>
239 <div class="mt-3">
240 <div class="form-group">
241 <label for="new-sec-title">Title</label>
242 <input type="text" id="new-sec-title" placeholder="e.g. Features, Installation..." autocomplete="off">
243 </div>
244 <div class="form-group">
245 <label for="new-sec-body">Body (Markdown)</label>
246 <textarea id="new-sec-body" rows="6" placeholder="Section content..."></textarea>
247 <button type="button" class="btn-secondary insert-image-btn" onclick="mediaPickerOpen('new-sec-body')">Insert Image</button>
248 </div>
249 <button type="button" class="btn-secondary" id="add-sec-btn">Add Section</button>
250 <span id="sec-add-status" class="ml-2 text-sm"></span>
251 </div>
252 </details>
253
254 <!-- Edit modal (hidden, shown inline when editing) -->
255 <div id="section-edit-modal" class="section-edit-modal hidden">
256 <input type="hidden" id="edit-sec-id">
257 <div class="form-group">
258 <label for="edit-sec-title">Title</label>
259 <input type="text" id="edit-sec-title" autocomplete="off">
260 </div>
261 <div class="form-group">
262 <label for="edit-sec-body">Body (Markdown)</label>
263 <textarea id="edit-sec-body" rows="6"></textarea>
264 <button type="button" class="btn-secondary insert-image-btn" onclick="mediaPickerOpen('edit-sec-body')">Insert Image</button>
265 </div>
266 <div class="section-edit-actions">
267 <button type="button" class="btn-primary btn-compact" id="save-sec-btn">Save</button>
268 <button type="button" class="btn-secondary btn-compact" id="cancel-sec-btn">Cancel</button>
269 </div>
270 <span id="sec-edit-status" class="ml-2 text-sm"></span>
271 </div>
272 </details>
273 <!-- Publishing (merged from Settings) -->
274 <div class="content-section">
275 <div class="section-header">
276 <h2 class="subsection-title">Publishing</h2>
277 </div>
278
279 {% if let Some(scheduled) = item.publish_at %}
280 <p class="publish-note">
281 Scheduled for {{ scheduled }}.
282 </p>
283 <div class="action-buttons">
284 <button class="btn-secondary"
285 hx-put="/api/items/{{ item.id }}"
286 hx-vals='{"publish_at": ""}'
287 hx-confirm="Cancel the scheduled publish?"
288 hx-on::after-request="if(event.detail.successful) document.getElementById('tab-details').click()">Cancel Schedule</button>
289 </div>
290 {% else if item.is_public %}
291 <p class="publish-note">
292 Unpublishing removes this item from public view but preserves all data.
293 </p>
294 <div class="action-buttons">
295 <button class="btn-secondary"
296 hx-put="/api/items/{{ item.id }}"
297 hx-vals='{"is_public": "false"}'
298 hx-confirm="Unpublish this item? It will be hidden from public view."
299 hx-on::after-request="if(event.detail.successful) document.getElementById('tab-details').click()">Unpublish Item</button>
300 </div>
301 {% else %}
302 <p class="publish-note">
303 Publish this item to make it visible to the public.
304 </p>
305 <div class="action-buttons">
306 <button class="btn-primary"
307 hx-put="/api/items/{{ item.id }}"
308 hx-vals='{"is_public": "true"}'
309 hx-confirm="Publish this item?"
310 hx-on::after-request="if(event.detail.successful) document.getElementById('tab-details').click()">Publish Now</button>
311 <button class="btn-secondary" onclick="document.getElementById('schedule-form').classList.remove('hidden')">Schedule</button>
312 </div>
313 <form id="schedule-form" class="hidden mt-4"
314 hx-put="/api/items/{{ item.id }}"
315 hx-on:htmx:config-request="var v=event.detail.parameters.publish_at; if(v){ event.detail.parameters.publish_at=new Date(v).toISOString(); }"
316 hx-on::after-request="if(event.detail.successful) document.getElementById('tab-details').click()">
317 <div class="form-group">
318 <label for="publish-at">Publish at</label>
319 <input type="datetime-local" id="publish-at" name="publish_at" required>
320 <div class="hint">Uses your computer's time zone.</div>
321 </div>
322 <button type="submit" class="btn-primary">Schedule Publish</button>
323 </form>
324 {% endif %}
325 </div>
326
327 </div>
328