Diff -- /var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/TEMPLATES/webstls_edit_model.html
Diff

/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/TEMPLATES/webstls_edit_model.html

added on local at 2026-07-01 13:48:02

Added
+337
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to ef61ca39f926
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1<!-- =====================================================================
2 Edit model details. Loaded from /edit_model.cgi?id=N, POSTs back
3 to itself, redirects to /models.cgi on save.
4===================================================================== -->
5
6<style>
7 .em-grid { display:grid; grid-template-columns: 1.5fr 1fr; gap:24px; align-items:flex-start; }
8 @media (max-width: 1000px) { .em-grid { grid-template-columns: 1fr; } }
9 .em-side-hero { aspect-ratio: 4/3; background-size: cover; background-position: center; border-radius: 12px; border: 1px solid var(--col-border); }
10 .em-side-note { font-size: 11px; color: var(--col-text-3); margin-top: 8px; line-height: 1.5; }
11 .em-side-note a { color: var(--col-accent-bright); text-decoration: none; }
12 .em-status-row { display: flex; gap: 8px; flex-wrap: wrap; }
13 .em-status-row label { flex: 1; min-width: 130px; padding: 10px 12px; border: 1px solid var(--col-border); border-radius: 8px; cursor: pointer; font-size: 13px; color: var(--col-text-2); display: flex; align-items: center; gap: 8px; transition: all 0.15s; }
14 .em-status-row label:has(input:checked) { border-color: var(--col-accent); background: rgba(124,58,237,0.08); color: var(--col-text); }
15
16 /* Hero replace/pan affordance -- mirrors the storefront editor. */
17 #em-hero { cursor: grab; position: relative; user-select: none; transition: outline-color 0.15s; outline: 2px solid transparent; outline-offset: 4px; }
18 #em-hero:hover { outline-color: rgba(124,58,237,0.65); }
19 #em-hero.we-dragging { cursor: grabbing !important; outline-color: #c084fc; }
20 #em-hero::after {
21 content: "Click to replace . Drag to reposition";
22 position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
23 background: rgba(0,0,0,0.75); color: #fff;
24 padding: 6px 14px; border-radius: 6px;
25 font-size: 12px; font-family: ui-monospace, monospace;
26 letter-spacing: 1px; text-transform: uppercase;
27 opacity: 0; transition: opacity 0.15s;
28 pointer-events: none; white-space: nowrap;
29 }
30 #em-hero:hover::after { opacity: 1; }
31 #em-hero.we-dragging::after { opacity: 0; }
32 #em-hero.is-saving { opacity: 0.6; }
33 #em-hero.is-saved { box-shadow: 0 0 0 2px #10b981 inset; }
34</style>
35
36<div class="page-head">
37 <div>
38 <span class="page-eyebrow"><a href="/models.cgi" class="text-brand">&larr; Models</a></span>
39 <h1 class="page-title">Edit model</h1>
40 <p class="page-subtitle">Update the details. Changes take effect across your storefront and queued marketplace pushes.</p>
41 </div>
42 <div class="page-actions">
43 <a class="btn btn-secondary" href="/models.cgi">Cancel</a>
44 <button type="submit" form="emForm" class="btn btn-primary">Save changes</button>
45 </div>
46</div>
47
48<form id="emForm" method="post" action="/edit_model.cgi">
49<input type="hidden" name="id" value="$model_id">
50
51<div class="em-grid">
52
53 <!-- ============ LEFT: editable fields ============ -->
54 <div style="display:flex;flex-direction:column;gap:16px">
55
56 <div class="module">
57 <div class="module-head"><div class="left">
58 <div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></div>
59 <div><div class="module-title">Listing details</div><div class="module-sub">What buyers see when they land on this model.</div></div>
60 </div></div>
61 <div class="module-body">
62 <div class="form-group">
63 <label class="form-label">Title <span class="req">*</span></label>
64 <input class="input" name="title" value="$title" maxlength="200" required>
65 </div>
66 <div class="form-group">
67 <label class="form-label">Short tagline</label>
68 <input class="input" name="tagline" value="$tagline" maxlength="180" placeholder="One-line summary shown on cards">
69 </div>
70 <div class="form-group">
71 <label class="form-label">Full description</label>
72 <textarea class="textarea" name="description" rows="8" placeholder="Detailed description, materials, scale, what's included...">$description</textarea>
73 </div>
74 <div class="dash-grid-2" style="gap:14px">
75 <div class="form-group">
76 <label class="form-label">Category</label>
77 <select class="select" name="category">
78 <option value="">&mdash; Select &mdash;</option>
79 [loop:@category_options]
80 <option value="$loop1.value" $loop1.selected>$loop1.label</option>
81 [/loop]
82 </select>
83 </div>
84 <div class="form-group">
85 <label class="form-label">License</label>
86 <select class="select" name="license_type">
87 <option value="personal">Personal use only</option>
88 <option value="personal_attrib">Personal + attribution required</option>
89 <option value="commercial">Commercial use allowed</option>
90 <option value="cc_by">Creative Commons CC-BY</option>
91 <option value="cc_by_nc">Creative Commons CC-BY-NC</option>
92 <option value="cc_by_sa">Creative Commons CC-BY-SA</option>
93 <option value="cc0">CC0 (Public Domain)</option>
94 </select>
95 </div>
96 </div>
97 </div>
98 </div>
99
100 <div class="module">
101 <div class="module-head"><div class="left">
102 <div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg></div>
103 <div><div class="module-title">Price</div><div class="module-sub">Base price in USD. WebSTLs takes 15% on paid sales.</div></div>
104 </div></div>
105 <div class="module-body">
106 <div class="form-group">
107 <label class="form-label">Price (\$)</label>
108 <input type="text" class="input" name="price" value="$price" placeholder="0.00">
109 </div>
110 </div>
111 </div>
112
113 </div>
114
115 <!-- ============ RIGHT: status, visibility, hero preview ============ -->
116 <div style="display:flex;flex-direction:column;gap:16px">
117
118 <div class="module">
119 <div class="module-head"><div class="left">
120 <div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg></div>
121 <div><div class="module-title">Status</div><div class="module-sub">Draft = nobody can see it. Published = live everywhere.</div></div>
122 </div></div>
123 <div class="module-body">
124 <select class="select" name="status">
125 <option value="draft" $sel_status_draft>Draft &mdash; only you can see it</option>
126 <option value="published" $sel_status_published>Published &mdash; live on your storefront</option>
127 <option value="unlisted" $sel_status_unlisted>Unlisted &mdash; reachable by direct link only</option>
128 <option value="archived" $sel_status_archived>Archived &mdash; hidden, kept in your account</option>
129 </select>
130 </div>
131 </div>
132
133 <div class="module">
134 <div class="module-head"><div class="left">
135 <div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></div>
136 <div><div class="module-title">Visibility</div><div class="module-sub">Discoverable in catalogs and search?</div></div>
137 </div></div>
138 <div class="module-body">
139 <div class="em-status-row">
140 <label><input type="radio" name="visibility" value="public" $sel_vis_public> Public</label>
141 <label><input type="radio" name="visibility" value="unlisted" $sel_vis_unlisted> Unlisted</label>
142 <label><input type="radio" name="visibility" value="private" $sel_vis_private> Private</label>
143 </div>
144 </div>
145 </div>
146
147 <div class="module">
148 <div class="module-head"><div class="left">
149 <div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg></div>
150 <div><div class="module-title">Hero image</div><div class="module-sub">Click to replace. Drag to set the focal point.</div></div>
151 </div></div>
152 <div class="module-body">
153 <div class="em-side-hero" id="em-hero"
154 data-we-editable="image"
155 data-we-field="hero_image_url"
156 data-we-target="model"
157 data-we-target-id="$model_id"
158 data-we-pos-field="hero_image_pos"
159 data-we-pos="$hero_image_pos"
160 style="background-image:url('$hero_image');background-position:$hero_image_pos"></div>
161 <p class="em-side-note">
162 The image saves to this model immediately on upload/drag &mdash; you do not need to hit Save changes for it.
163 </p>
164 </div>
165 </div>
166
167 <div class="module">
168 <div class="module-body" style="display:flex;flex-direction:column;gap:8px">
169 <button type="submit" class="btn btn-primary" style="width:100%">Save changes</button>
170 <a href="/models.cgi" class="btn btn-secondary" style="width:100%;text-align:center">Cancel</a>
171 </div>
172 </div>
173
174 </div>
175</div>
176
177</form>
178
179
180<script>
181/* =====================================================================
182 Hero replace + focal-point picker for /edit_model.cgi.
183 Click (no drag) -> file picker, upload via /upload_image.cgi, save
184 the resulting /img.cgi?id=... URL to models.hero_image_url via
185 /update_model_field.cgi. Drag -> pan, save background-position to
186 models.hero_image_pos. Mirrors the storefront editor behavior so
187 the experience is identical in both places.
188===================================================================== */
189(function() {
190 var hero = document.getElementById('em-hero');
191 if (!hero) return;
192 var mid = hero.getAttribute('data-we-target-id');
193 if (!mid) return;
194
195 function postText(url, fd) {
196 return fetch(url, { method: 'POST', body: fd, credentials: 'same-origin' })
197 .then(function(r){
198 return r.text().then(function(body){
199 try { return JSON.parse(body); }
200 catch (e) {
201 var snippet = body ? body.replace(/<[^>]*>/g,' ').replace(/\s+/g,' ').trim().slice(0, 200) : '(empty body)';
202 throw new Error('server returned ' + r.status + ' ' + r.statusText + ' (' + snippet + ')');
203 }
204 });
205 });
206 }
207
208 function saveModelField(field, value) {
209 var fd = new FormData();
210 fd.append('field', field);
211 fd.append('value', value);
212 fd.append('model_id', mid);
213 return postText('/update_model_field.cgi', fd);
214 }
215
216 function flash(el, klass) {
217 el.classList.add(klass);
218 setTimeout(function(){ el.classList.remove(klass); }, 1200);
219 }
220
221 function readBgPos(el) {
222 var raw = el.dataset.wePos || el.style.backgroundPosition || '50% 50%';
223 var parts = raw.split(/\s+/);
224 var x = parseFloat(parts[0]); if (isNaN(x)) x = 50;
225 var y = parseFloat(parts[1]); if (isNaN(y)) y = 50;
226 return [x, y];
227 }
228
229 /* ---- Drag-to-pan (Pointer Events: mouse + touch + pen) ----
230 Was mouse-only and broken on phones. Now: pointerdown begins
231 tracking, pointermove past the threshold is treated as a drag
232 (saves new position on release); below the threshold a release
233 is a tap -> open file picker. touch-action:none + the WebKit
234 suppressors stop iOS from popping its own long-press menu over
235 the hero or treating the gesture as a page scroll. */
236 hero.style.touchAction = 'none';
237 hero.style.webkitUserSelect = 'none';
238 hero.style.userSelect = 'none';
239 hero.style.webkitTouchCallout = 'none';
240 hero.style.webkitUserDrag = 'none';
241 var drag = null;
242 function dragStart(e) {
243 if (e.button !== undefined && e.button !== 0) return;
244 if (e.target.closest('a,button,input,select,textarea')) return;
245 e.preventDefault();
246 var bg = readBgPos(hero);
247 drag = {
248 pointerId: e.pointerId,
249 pointerType: e.pointerType || 'mouse',
250 startX: e.clientX, startY: e.clientY,
251 rect: hero.getBoundingClientRect(),
252 startBgX: bg[0], startBgY: bg[1],
253 moved: false, pos: null
254 };
255 try { hero.setPointerCapture(e.pointerId); } catch(_) {}
256 }
257 function dragMove(e) {
258 if (!drag || e.pointerId !== drag.pointerId) return;
259 var dx = e.clientX - drag.startX;
260 var dy = e.clientY - drag.startY;
261 /* Higher threshold on touch since finger jitter is naturally larger. */
262 var thresh = drag.pointerType === 'mouse' ? 5 : 12;
263 if (!drag.moved && Math.abs(dx) < thresh && Math.abs(dy) < thresh) return;
264 drag.moved = true;
265 hero.classList.add('we-dragging');
266 var r = drag.rect;
267 var px = drag.startBgX - (dx / r.width * 100);
268 var py = drag.startBgY - (dy / r.height * 100);
269 if (px < 0) px = 0; if (px > 100) px = 100;
270 if (py < 0) py = 0; if (py > 100) py = 100;
271 var pos = px.toFixed(1) + '% ' + py.toFixed(1) + '%';
272 drag.pos = pos;
273 hero.style.backgroundPosition = pos;
274 hero.dataset.wePos = pos;
275 }
276 function dragEnd(e) {
277 if (!drag || e.pointerId !== drag.pointerId) return;
278 var st = drag; drag = null;
279 hero.classList.remove('we-dragging');
280 if (st.moved && st.pos) {
281 hero.classList.add('is-saving');
282 saveModelField('hero_image_pos', st.pos)
283 .then(function(data){
284 hero.classList.remove('is-saving');
285 if (data && data.success) flash(hero, 'is-saved');
286 else appToast({ message: 'Reposition failed: ' + (data && data.error || 'unknown'), type: 'error' });
287 })
288 .catch(function(err){
289 hero.classList.remove('is-saving');
290 appToast({ message: 'Reposition failed: ' + err.message, type: 'error' });
291 });
292 return;
293 }
294 /* Pure click/tap -> open file picker */
295 openReplace();
296 }
297 hero.addEventListener('pointerdown', dragStart);
298 hero.addEventListener('pointermove', dragMove);
299 hero.addEventListener('pointerup', dragEnd);
300 hero.addEventListener('pointercancel', dragEnd);
301 hero.addEventListener('dragstart', function(e){ e.preventDefault(); });
302 hero.addEventListener('contextmenu', function(e){ e.preventDefault(); });
303
304 function openReplace() {
305 var input = document.createElement('input');
306 input.type = 'file';
307 input.accept = 'image/*';
308 input.onchange = function() {
309 if (!input.files || !input.files[0]) return;
310 var file = input.files[0];
311 var MAX = 10 * 1024 * 1024;
312 if (file.size > MAX) {
313 appToast({ message: 'Image is too large (' + (file.size / 1048576).toFixed(1) + ' MB). Max 10 MB.', type: 'warning' });
314 return;
315 }
316 hero.classList.add('is-saving');
317 var fd = new FormData();
318 fd.append('image', file);
319 postText('/upload_image.cgi', fd)
320 .then(function(data){
321 if (!data || !data.success || !data.url) throw new Error(data && data.error || 'upload failed');
322 return saveModelField('hero_image_url', data.url).then(function(saved){
323 if (!saved || !saved.success) throw new Error(saved && saved.error || 'save failed');
324 hero.style.backgroundImage = "url('" + data.url + "')";
325 hero.classList.remove('is-saving');
326 flash(hero, 'is-saved');
327 });
328 })
329 .catch(function(err){
330 hero.classList.remove('is-saving');
331 appToast({ message: 'Image change failed: ' + err.message, type: 'error' });
332 });
333 };
334 input.click();
335 }
336})();
337</script>
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help