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

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

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

Added
+455
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 29a8e6f01aab
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 Models management page. Cards per model with controls for:
3 - Feature (move to top of storefront)
4 - Show / Hide on storefront
5 - Move up / Move down in sort order
6 All write actions POST to /update_listing.cgi.
7===================================================================== -->
8
9<style>
10 .mod-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 18px; }
11 .mod-card {
12 position: relative;
13 background: var(--col-surface-1);
14 border: 1px solid var(--col-border);
15 border-radius: 12px;
16 overflow: hidden;
17 transition: border-color 0.15s, transform 0.15s, box-shadow 0.15s;
18 display: flex; flex-direction: column;
19 }
20 .mod-card:hover { border-color: rgba(124,58,237,0.45); transform: translateY(-2px); box-shadow: 0 12px 30px rgba(0,0,0,0.4); }
21 .mod-card.is-hidden { opacity: 0.55; }
22 .mod-card.is-saving { pointer-events: none; opacity: 0.5; }
23 .mod-hero { aspect-ratio: 4/3; background-size: cover; background-position: center; border-bottom: 1px solid var(--col-border); }
24 .mod-body { padding: 14px 16px; display: flex; flex-direction: column; gap: 8px; flex: 1; }
25 .mod-title { font-size: 15px; font-weight: 700; color: var(--col-text); line-height: 1.3; }
26 .mod-meta-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
27 .mod-price { font-size: 14px; font-weight: 700; color: var(--col-accent-bright); font-family: var(--font-mono, monospace); }
28 .mod-category { font-size: 11px; color: var(--col-text-3); letter-spacing: 1px; text-transform: uppercase; }
29 .mod-actions { padding: 10px 12px; border-top: 1px solid var(--col-border); display: flex; flex-direction: column; gap: 6px; background: var(--col-surface-2); }
30 .mod-act-row { display: grid; gap: 6px; }
31 .mod-act-row.r-2 { grid-template-columns: 1fr 1fr; }
32 .mod-act-row.r-4 { grid-template-columns: repeat(4, 1fr); }
33 .mod-act { background: none; border: 1px solid var(--col-border); border-radius: 6px; color: var(--col-text-2); cursor: pointer; padding: 7px 0; font-size: 11px; font-weight: 600; letter-spacing: 0.5px; text-transform: uppercase; font-family: inherit; transition: all 0.15s; text-decoration: none; display: flex; align-items: center; justify-content: center; gap: 4px; }
34 .mod-act:hover { border-color: var(--col-accent); color: var(--col-text); background: rgba(124,58,237,0.10); }
35 .mod-act:disabled { opacity: 0.35; cursor: not-allowed; }
36 .mod-act.is-feature { background: linear-gradient(130deg, #7c3aed, #3b82f6); color: #fff; border-color: transparent; }
37 .mod-act.is-feature:hover { filter: brightness(1.1); }
38 /* Make Edit Details visually weighted like Publish (the other
39 row-1 primary action). Higher background opacity + saturated
40 text color so it reads at a glance against the card surface
41 instead of blending in. */
42 .mod-act.is-edit { background: rgba(124,58,237,0.28); border-color: rgba(167,139,250,0.70); color: #c4b5fd; }
43 .mod-act.is-edit:hover { background: rgba(124,58,237,0.45); border-color: #a78bfa; color: #fff; }
44 .mod-act.is-publish { background: rgba(34,197,94,0.14); border-color: rgba(34,197,94,0.4); color: #4ade80; }
45 .mod-act.is-publish:hover { background: rgba(34,197,94,0.25); }
46 .mod-act.is-unpublish { background: rgba(245,158,11,0.12); border-color: rgba(245,158,11,0.35); color: #fbbf24; }
47 .mod-act.is-unpublish:hover { background: rgba(245,158,11,0.22); }
48 .mod-act.is-restore { background: rgba(34,197,94,0.14); border-color: rgba(34,197,94,0.45); color: #4ade80; }
49 .mod-act.is-restore:hover { background: rgba(34,197,94,0.26); }
50 .mod-card.is-trash { border-color: rgba(239,68,68,0.25); }
51 .mod-card.is-trash .mod-hero { filter: grayscale(0.6) brightness(0.7); }
52 .mod-corner-badge { position: absolute; top: 10px; left: 10px; z-index: 2; }
53 .mod-delete-btn {
54 position: absolute; top: 10px; right: 10px; z-index: 3;
55 width: 28px; height: 28px; border-radius: 50%;
56 background: rgba(0,0,0,0.55);
57 border: 1px solid rgba(255,255,255,0.12);
58 color: #fff; font-size: 14px; line-height: 1;
59 cursor: pointer; display: grid; place-items: center;
60 opacity: 0; transition: opacity 0.15s, background 0.15s, border-color 0.15s;
61 font-family: inherit;
62 }
63 .mod-card:hover .mod-delete-btn { opacity: 1; }
64 .mod-delete-btn:hover { background: #ef4444; border-color: #f87171; }
65 .mod-empty {
66 text-align: center; padding: 80px 32px; border: 1px dashed var(--col-border-2);
67 border-radius: 14px; color: var(--col-text-2);
68 }
69 .mod-empty h2 { font-size: 22px; margin-bottom: 8px; color: var(--col-text); }
70 .mod-empty p { color: var(--col-text-2); margin-bottom: 18px; }
71 .mod-stats { display: flex; gap: 14px; margin-bottom: 24px; flex-wrap: wrap; }
72 .mod-stat {
73 background: var(--col-surface-1);
74 border: 1px solid var(--col-border);
75 border-radius: 10px;
76 padding: 14px 18px;
77 min-width: 140px;
78 cursor: pointer;
79 transition: border-color 0.15s, transform 0.15s, background 0.15s;
80 text-decoration: none;
81 display: block;
82 }
83 .mod-stat:hover { border-color: rgba(124,58,237,0.55); transform: translateY(-2px); }
84 .mod-stat.is-active {
85 border-color: var(--col-accent);
86 background: linear-gradient(130deg, rgba(124,58,237,0.12), rgba(59,130,246,0.06));
87 box-shadow: 0 6px 18px rgba(124,58,237,0.18);
88 }
89 .mod-stat-lbl { font-size: 10px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--col-text-3); margin-bottom: 4px; font-weight: 700; }
90 .mod-stat-val { font-size: 24px; font-weight: 700; color: var(--col-text); }
91 .mod-stat-val.green { color: #4ade80; }
92 .mod-stat-val.amber { color: #fbbf24; }
93 .mod-stat-val.dim { color: var(--col-text-3); }
94 .mod-stat-val.red { color: #f87171; }
95
96 .mod-filter-bar {
97 display: flex; align-items: center; justify-content: space-between;
98 margin-bottom: 12px; gap: 12px;
99 font-size: 12px; color: var(--col-text-3);
100 letter-spacing: 1px; text-transform: uppercase;
101 }
102 .mod-filter-bar .mod-filter-name { color: var(--col-text-2); font-weight: 700; }
103 .mod-filter-bar a { color: var(--col-accent-bright); text-decoration: none; font-weight: 700; }
104</style>
105
106<div class="page-head">
107 <div>
108 <span class="page-eyebrow"><span class="dot"></span> Models</span>
109 <h1 class="page-title">Your designs</h1>
110 <p class="page-subtitle">Manage which models appear on your storefront, the order they show up, and which one is featured. $model_count total &middot; $listed_count on storefront &middot; $hidden_count hidden.</p>
111 </div>
112 <div class="page-actions">
113 <a href="/upload_model.cgi" class="btn btn-primary" id="modUploadCta" title="Open the upload form to add a new model (digital file, physical print, or both)">
114 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
115 Upload new model
116 </a>
117 </div>
118</div>
119
120<div class="mod-stats">
121 <a class="mod-stat[if:$is_filter_all] is-active[/if]" href="/models.cgi" title="All models you own (drafts + published + hidden, excludes trash)">
122 <div class="mod-stat-lbl">All models</div>
123 <div class="mod-stat-val">$model_count</div>
124 </a>
125 <a class="mod-stat[if:$is_filter_pub] is-active[/if]" href="/models.cgi?filter=published" title="Published AND currently visible on your storefront">
126 <div class="mod-stat-lbl">On storefront</div>
127 <div class="mod-stat-val green">$listed_count</div>
128 </a>
129 <a class="mod-stat[if:$is_filter_drafts] is-active[/if]" href="/models.cgi?filter=drafts" title="Saved as draft -- nobody can see these except you">
130 <div class="mod-stat-lbl">Drafts</div>
131 <div class="mod-stat-val amber">$drafts_count</div>
132 </a>
133 <a class="mod-stat[if:$is_filter_hidden] is-active[/if]" href="/models.cgi?filter=hidden" title="Published but you toggled them off your storefront with Hide">
134 <div class="mod-stat-lbl">Hidden</div>
135 <div class="mod-stat-val dim">$hidden_count</div>
136 </a>
137 [if:$has_trash]
138 <a class="mod-stat[if:$is_filter_trash] is-active[/if]" href="/models.cgi?filter=trash" title="Deleted models -- restore from here">
139 <div class="mod-stat-lbl">Trash</div>
140 <div class="mod-stat-val red">$trash_count</div>
141 </a>
142 [/if]
143</div>
144
145<div class="mod-filter-bar">
146 <span><span class="mod-filter-name">Showing:</span> $filter_label</span>
147 <span style="display:flex;gap:14px;align-items:center">
148 [if:$is_filter_trash][if:$has_models]
149 <button type="button" id="modEmptyTrash" style="background:rgba(239,68,68,0.10);border:1px solid rgba(239,68,68,0.35);color:#f87171;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:11px;font-weight:700;letter-spacing:1px;text-transform:uppercase;font-family:inherit;transition:all 0.15s">
150 Empty trash
151 </button>
152 [/if][/if]
153 [if:!$is_filter_all]<a href="/models.cgi">Clear filter &times;</a>[/if]
154 </span>
155</div>
156
157[if:$has_models]
158
159<div class="mod-grid">
160 [loop:@models]
161 <div class="mod-card[if:!$loop1.is_visible] is-hidden[/if][if:$loop1.is_removed] is-trash[/if]" data-mid="$loop1.id" data-title="$loop1.title">
162 <div class="mod-corner-badge">$loop1.status_pill</div>
163 [ptag:edit_models]
164 [if:!$loop1.is_removed]
165 <button type="button" class="mod-delete-btn" data-model-action="delete" title="Delete this model (moves to Trash)">&times;</button>
166 [/if]
167 [/ptag:edit_models]
168 [ptag:delete_models]
169 [if:$loop1.is_removed]
170 <button type="button" class="mod-delete-btn" data-model-action="purge" title="Permanently delete this model. Removes it from your trash and you will not be able to restore it.">&times;</button>
171 [/if]
172 [/ptag:delete_models]
173 <div class="mod-hero" style="background-image:url('$loop1.hero_image');background-position:$loop1.hero_pos"></div>
174 <div class="mod-body">
175 <div class="mod-title">$loop1.title</div>
176 <div class="mod-meta-row">
177 <span class="mod-price">$loop1.price</span>
178 <span class="mod-category">$loop1.category</span>
179 </div>
180 </div>
181 <div class="mod-actions">
182 [if:$loop1.is_removed]
183 <div class="mod-act-row r-2">
184 [ptag:edit_models]
185 <button type="button" class="mod-act is-restore" data-model-action="restore" title="Restore this model -- moves it back to drafts">
186 &#8634; Restore to drafts
187 </button>
188 [/ptag:edit_models]
189 <a class="mod-act is-edit" href="/upload_model.cgi?model_id=$loop1.id" title="Open the editor to inspect what was in this model">
190 View details
191 </a>
192 </div>
193 [/if]
194 [if:!$loop1.is_removed]
195 <div class="mod-act-row r-2">
196 [ptag:edit_models]
197 <a class="mod-act is-edit" href="/upload_model.cgi?model_id=$loop1.id" title="Edit title, description, price, images, files, status, etc.">
198 <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5Z"/></svg>
199 Edit details
200 </a>
201 [if:$loop1.is_draft]
202 <button type="button" class="mod-act is-publish" data-model-action="status" data-value="published" title="Publish this model so it can appear on your storefront and other marketplaces">
203 &#9656; Publish
204 </button>
205 [/if]
206 [if:!$loop1.is_draft]
207 <button type="button" class="mod-act is-unpublish" data-model-action="status" data-value="draft" title="Move back to draft. The model stays in your account but is hidden from buyers everywhere.">
208 &#9646;&#9646; Unpublish
209 </button>
210 [/if]
211 [/ptag:edit_models]
212 </div>
213 <div class="mod-act-row r-4">
214 <button type="button" class="mod-act is-feature" data-listing-action="feature" title="Make this the spotlight featured product">&#9733; Feature</button>
215 <button type="button" class="mod-act" data-listing-action="toggle_visible" title="Show / hide on YOUR storefront (separate from publish status)">[if:$loop1.is_visible]Hide[/if][if:!$loop1.is_visible]Show[/if]</button>
216 <button type="button" class="mod-act" data-listing-action="move_up" title="Move up in storefront order">&#8593; Up</button>
217 <button type="button" class="mod-act" data-listing-action="move_down" title="Move down in storefront order">&#8595; Down</button>
218 </div>
219 [/if]
220 </div>
221 </div>
222 [/loop]
223</div>
224[/if]
225
226[if:!$has_models]
227[if:$is_filter_trash]
228<div class="mod-empty">
229 <h2>Trash is empty</h2>
230 <p>Deleted models would appear here. Nothing to restore right now.</p>
231 <a href="/models.cgi" class="btn btn-secondary">Back to all models</a>
232</div>
233[/if]
234[if:$is_filter_drafts]
235<div class="mod-empty">
236 <h2>No drafts</h2>
237 <p>Drafts are models you have saved but not yet published. You don't have any right now.</p>
238 <a href="/models.cgi" class="btn btn-secondary">Back to all models</a>
239</div>
240[/if]
241[if:$is_filter_pub]
242<div class="mod-empty">
243 <h2>Nothing on your storefront</h2>
244 <p>Published models that are visible on your storefront would appear here.</p>
245 <a href="/models.cgi" class="btn btn-secondary">Back to all models</a>
246</div>
247[/if]
248[if:$is_filter_hidden]
249<div class="mod-empty">
250 <h2>No hidden models</h2>
251 <p>This shows models you have published but toggled off your storefront with Hide. Nothing matches.</p>
252 <a href="/models.cgi" class="btn btn-secondary">Back to all models</a>
253</div>
254[/if]
255[if:$is_filter_all]
256<div class="mod-empty">
257 <h2>No models yet</h2>
258 <p>Upload your first design and it will appear here, automatically listed on your storefront.</p>
259 <a href="/upload_model.cgi" class="btn btn-primary">Upload your first model</a>
260</div>
261[/if]
262[/if]
263
264
265<script>
266(function() {
267 /* ---- Empty trash (bulk purge). Visible only on the Trash filter. ---- */
268 var emptyBtn = document.getElementById('modEmptyTrash');
269 if (emptyBtn) {
270 emptyBtn.addEventListener('mouseenter', function(){ emptyBtn.style.background = 'rgba(239,68,68,0.22)'; });
271 emptyBtn.addEventListener('mouseleave', function(){ emptyBtn.style.background = 'rgba(239,68,68,0.10)'; });
272 emptyBtn.addEventListener('click', function() {
273 appConfirm({
274 title: 'Empty trash?',
275 message: 'Every model currently in your trash will be permanently deleted from your account. You will not be able to restore them after confirming.',
276 confirm_label: 'Empty trash',
277 confirm_style: 'danger'
278 }).then(function(ok) {
279 if (!ok) return;
280 emptyBtn.disabled = true;
281 emptyBtn.textContent = 'Emptying...';
282 var fd = new FormData();
283 fd.append('all', '1');
284 fetch('/purge_models.cgi', { method: 'POST', body: fd, credentials: 'same-origin' })
285 .then(function(r){
286 return r.text().then(function(body){
287 try { return JSON.parse(body); }
288 catch (e) {
289 var snippet = body ? body.replace(/<[^>]*>/g,' ').replace(/\s+/g,' ').trim().slice(0, 200) : '(empty body)';
290 throw new Error('server returned ' + r.status + ' ' + r.statusText + ' (' + snippet + ')');
291 }
292 });
293 })
294 .then(function(data){
295 if (!data || !data.success) throw new Error(data && data.error || 'empty failed');
296 window.location.href = '/models.cgi';
297 })
298 .catch(function(err){
299 emptyBtn.disabled = false;
300 emptyBtn.textContent = 'Empty trash';
301 appToast({ message: 'Empty trash failed: ' + err.message, type: 'error' });
302 });
303 });
304 });
305 }
306
307 function postJson(url, fd, card) {
308 card.classList.add('is-saving');
309 return fetch(url, { method: 'POST', body: fd, credentials: 'same-origin' })
310 .then(function(r){
311 return r.text().then(function(body){
312 try { return JSON.parse(body); }
313 catch (e) {
314 var snippet = body ? body.replace(/<[^>]*>/g,' ').replace(/\s+/g,' ').trim().slice(0, 200) : '(empty body)';
315 throw new Error('server returned ' + r.status + ' ' + r.statusText + ' (' + snippet + ')');
316 }
317 });
318 })
319 .then(function(data){
320 if (!data || !data.success) throw new Error(data && data.error || 'action failed');
321 return data;
322 });
323 }
324
325 /* All listing actions (feature, hide/show, reorder) -> update_listing.cgi */
326 document.addEventListener('click', function(e) {
327 var btn = e.target.closest('[data-listing-action]');
328 if (!btn) return;
329 var card = btn.closest('.mod-card');
330 if (!card) return;
331 var mid = card.getAttribute('data-mid');
332 var action = btn.getAttribute('data-listing-action');
333 if (!mid || !action) return;
334
335 var fd = new FormData();
336 fd.append('action', action);
337 fd.append('model_id', mid);
338
339 postJson('/update_listing.cgi', fd, card)
340 .then(function(){
341 if (action === 'feature' || action === 'move_up' || action === 'move_down') {
342 window.location.reload();
343 return;
344 }
345 if (action === 'toggle_visible') {
346 var visBtn = card.querySelector('[data-listing-action="toggle_visible"]');
347 var nowHidden = card.classList.toggle('is-hidden');
348 if (visBtn) visBtn.textContent = nowHidden ? 'Show' : 'Hide';
349 card.classList.remove('is-saving');
350 return;
351 }
352 card.classList.remove('is-saving');
353 })
354 .catch(function(err){
355 card.classList.remove('is-saving');
356 appToast({ message: 'Action failed: ' + err.message, type: 'error' });
357 });
358 });
359
360 /* Model-level actions (status flip, delete) -> update_model_field.cgi.
361 "delete" is special: it confirms first, then maps to status=removed
362 (soft delete -- the row stays in the DB, every query filters
363 status='removed' out so the model disappears from storefront,
364 models page, marketplaces, etc.). */
365 document.addEventListener('click', function(e) {
366 var btn = e.target.closest('[data-model-action]');
367 if (!btn) return;
368 var card = btn.closest('.mod-card');
369 if (!card) return;
370 var mid = card.getAttribute('data-mid');
371 var action = btn.getAttribute('data-model-action');
372 var value = btn.getAttribute('data-value');
373 if (!mid || !action) return;
374
375 var field, endpoint = '/update_model_field.cgi';
376 if (action === 'delete') {
377 var title = card.getAttribute('data-title') || 'this model';
378 appConfirm({
379 title: 'Delete "' + title + '"?',
380 message: 'It will disappear from your storefront and every marketplace push immediately. You can restore it later from the Trash tab on this page.',
381 confirm_label: 'Move to trash',
382 confirm_style: 'warning'
383 }).then(function(ok) {
384 if (!ok) return;
385 var fd = new FormData();
386 fd.append('field', 'status');
387 fd.append('value', 'removed');
388 fd.append('model_id', mid);
389 postJson(endpoint, fd, card)
390 .then(function(){ window.location.reload(); })
391 .catch(function(err){
392 card.classList.remove('is-saving');
393 appToast({ message: 'Action failed: ' + err.message, type: 'error' });
394 });
395 });
396 return;
397 } else if (action === 'restore') {
398 /* Non-destructive -- no confirm needed. */
399 field = 'status';
400 value = 'draft';
401 } else if (action === 'purge') {
402 /* Permanent (from the user's perspective). Big confirm required. */
403 var title2 = card.getAttribute('data-title') || 'this model';
404 appConfirm({
405 title: 'Permanently delete "' + title2 + '"?',
406 message: 'This removes it from your trash for good. You will not be able to restore it from the page after confirming.',
407 confirm_label: 'Delete forever',
408 confirm_style: 'danger'
409 }).then(function(ok) {
410 if (!ok) return;
411 var fdPurge = new FormData();
412 fdPurge.append('model_id', mid);
413 postJson('/purge_models.cgi', fdPurge, card)
414 .then(function(){
415 card.style.transition = 'opacity 0.25s, transform 0.25s';
416 card.style.opacity = '0';
417 card.style.transform = 'scale(0.95)';
418 setTimeout(function(){ window.location.reload(); }, 260);
419 })
420 .catch(function(err){
421 card.classList.remove('is-saving');
422 appToast({ message: 'Permanent delete failed: ' + err.message, type: 'error' });
423 });
424 });
425 return;
426 } else {
427 field = action;
428 }
429
430 var fd = new FormData();
431 fd.append('field', field);
432 fd.append('value', value || '');
433 fd.append('model_id', mid);
434
435 postJson(endpoint, fd, card)
436 .then(function(){
437 if (action === 'delete' || action === 'restore') {
438 /* Fade the card out -- on delete it moves to trash, on
439 restore it moves back to drafts. Either way the current
440 view changes, so a reload is the easiest correct state. */
441 card.style.transition = 'opacity 0.25s, transform 0.25s';
442 card.style.opacity = '0';
443 card.style.transform = 'scale(0.95)';
444 setTimeout(function(){ window.location.reload(); }, 260);
445 return;
446 }
447 window.location.reload();
448 })
449 .catch(function(err){
450 card.classList.remove('is-saving');
451 appToast({ message: 'Action failed: ' + err.message, type: 'error' });
452 });
453 });
454})();
455</script>
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help