Diff -- /var/www/vhosts/webstls.com/httpdocs/TEMPLATES/webstls_bundles.html
Diff

/var/www/vhosts/webstls.com/httpdocs/TEMPLATES/webstls_bundles.html

added on WebSTLs (webstls.com) at 2026-07-01 22:26:57

Added
+293
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 7b98600138ec
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1<!-- Seller-side bundle builder. Backed by /bundles.cgi. -->
2
3<div class="page-head">
4 <div>
5 <span class="page-eyebrow"><span class="dot"></span> Studio &middot; Packages</span>
6 <h1 class="page-title">Bundles</h1>
7 <p class="page-subtitle">Group several of your listings into one combo with a discounted price. Buyers see the bundle as its own card on your storefront; the individual items keep selling on their own too.</p>
8 </div>
9 [if:!$is_editing]
10 <div class="page-actions">
11 <a href="/bundles.cgi?id=new" class="btn btn-primary" id="bunNew">
12 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
13 New bundle
14 </a>
15 </div>
16 [/if]
17</div>
18
19[if:$has_flash]
20<div class="banner $flash_kind mb-3">
21 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/></svg>
22 <div class="text-sm fw-600">$flash_msg</div>
23</div>
24[/if]
25
26<style>
27 .bun-list { display:grid; gap:12px; }
28 .bun-row { display:flex; align-items:center; gap:14px; padding:14px 18px; background:var(--col-surface-1); border:1px solid var(--col-border); border-radius:12px; }
29 .bun-row .info { flex:1; min-width:0; }
30 .bun-row .title { font-size:16px; font-weight:700; color:var(--col-text); }
31 .bun-row .meta { font-size:12px; color:var(--col-text-3); margin-top:3px; }
32 .bun-row .price { font-family:var(--font-mono,monospace); font-weight:700; color:var(--col-accent-bright); }
33 .bun-row .actions { display:flex; gap:6px; }
34 .bun-pill { font-size:10px; padding:3px 9px; border-radius:5px; letter-spacing:1px; font-weight:700; text-transform:uppercase; }
35 .bun-pill.draft { background:rgba(156,163,175,0.18); color:#9ca3af; }
36 .bun-pill.published { background:rgba(34,197,94,0.18); color:#86efac; }
37 .bun-pill.archived { background:rgba(239,68,68,0.18); color:#fca5a5; }
38 .bun-kind { font-size:10px; padding:3px 9px; border-radius:5px; letter-spacing:1px; font-weight:700; text-transform:uppercase; background:rgba(59,130,246,0.15); color:#60a5fa; }
39 .bun-kind.physical { background:rgba(217,119,6,0.15); color:#f59e0b; }
40 .bun-kind.both { background:rgba(168,85,247,0.15); color:#c084fc; }
41
42 /* Picker */
43 .picker-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(220px, 1fr)); gap:8px; max-height:380px; overflow-y:auto; padding:6px; border:1px solid var(--col-border); border-radius:10px; background:var(--col-surface-2); }
44 .picker-card { display:flex; align-items:center; gap:8px; padding:8px 10px; border:1px solid var(--col-border); border-radius:8px; background:var(--col-surface-1); cursor:pointer; transition:all .15s; }
45 .picker-card:hover { border-color:var(--col-accent); }
46 .picker-card.is-on { border-color:var(--col-accent); background:var(--grad-brand-soft); }
47 .picker-card input { display:none; }
48 .picker-title { flex:1; font-size:12px; font-weight:600; color:var(--col-text); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
49 .picker-price { font-family:var(--font-mono,monospace); font-size:11px; color:var(--col-accent-bright); }
50</style>
51
52[if:!$is_editing]
53[if:!$has_bundles]
54<div style="padding:60px 24px;text-align:center;border:1px dashed var(--col-border-2);border-radius:14px;color:var(--col-text-2)">
55 <div style="font-size:48px;margin-bottom:8px">[]</div>
56 <h2 style="color:#fff;font-family:var(--font-display,inherit);margin-bottom:8px">No bundles yet</h2>
57 <p>Bundle 2 or more of your listings into a single combo deal &mdash; discount the price, give buyers a "buy them all" path. Works for digital downloads, printed boxes, or both.</p>
58 <a href="/bundles.cgi?id=new" class="btn btn-primary" style="margin-top:14px">Create your first bundle</a>
59</div>
60[/if]
61
62[if:$has_bundles]
63<div class="bun-list">
64 [loop:@bundles]
65 <div class="bun-row">
66 <div class="info">
67 <div class="title">$loop1.title</div>
68 <div class="meta">
69 <span class="bun-pill $loop1.status">$loop1.status</span>
70 <span class="bun-kind $loop1.kind">$loop1.kind</span>
71 <span style="margin-left:6px">$loop1.item_count item(s)</span>
72 [if:$loop1.tagline] &middot; <span>$loop1.tagline</span>[/if]
73 </div>
74 </div>
75 <div class="price">$loop1.price_label</div>
76 <div class="actions">
77 <a href="/bundles.cgi?id=$loop1.id" class="btn btn-secondary btn-sm">Edit</a>
78 [if:!$loop1.is_published]
79 <form method="POST" action="/bundles.cgi" style="display:inline">
80 <input type="hidden" name="_act" value="publish">
81 <input type="hidden" name="id" value="$loop1.id">
82 <button class="btn btn-primary btn-sm" type="submit">Publish</button>
83 </form>
84 [/if]
85 [if:$loop1.is_published]
86 <form method="POST" action="/bundles.cgi" style="display:inline">
87 <input type="hidden" name="_act" value="archive">
88 <input type="hidden" name="id" value="$loop1.id">
89 <button class="btn btn-secondary btn-sm" type="submit">Archive</button>
90 </form>
91 [/if]
92 </div>
93 </div>
94 [/loop]
95</div>
96[/if]
97[/if]
98
99[if:$is_editing]
100<form id="bunForm" method="POST" action="/bundles.cgi">
101 [if:$is_new_form]<input type="hidden" name="_act" value="create">[/if]
102 [if:!$is_new_form]<input type="hidden" name="_act" value="update">[/if]
103 [if:!$is_new_form]<input type="hidden" name="id" value="$edit_id">[/if]
104 <input type="hidden" name="item_ids" id="bunItemIds" value="">
105
106 <div class="dash-grid" style="grid-template-columns:2fr 1fr;gap:16px">
107 <div style="display:flex;flex-direction:column;gap:16px">
108
109 <div class="module">
110 <div class="module-head"><div class="left">
111 <div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg></div>
112 <div><div class="module-title">Bundle details</div><div class="module-sub">What buyers see on the storefront card.</div></div>
113 </div></div>
114 <div class="module-body">
115 <div class="form-group">
116 <label class="form-label">Title <span class="req">*</span></label>
117 <input type="text" name="title" class="input" required maxlength="200" placeholder="e.g. Sci-Fi Mega Pack" value="$title">
118 </div>
119 <div class="form-group">
120 <label class="form-label">Tagline</label>
121 <input type="text" name="tagline" class="input" maxlength="180" placeholder="One-line summary on listing cards" value="$tagline">
122 </div>
123 <div class="form-group">
124 <label class="form-label">Description</label>
125 <textarea name="description" class="textarea" rows="5" placeholder="Sell the bundle. Why does this set hang together? What does the buyer save by buying it as a pack?">$description</textarea>
126 </div>
127 </div>
128 </div>
129
130 <div class="module">
131 <div class="module-head"><div class="left">
132 <div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/></svg></div>
133 <div><div class="module-title">Bundle items</div><div class="module-sub">Pick 2+ of your listings. The buyer gets all of them when they purchase the bundle.</div></div>
134 </div></div>
135 <div class="module-body">
136 [if:!$has_candidates]
137 <p class="text-xs text-dim">Upload at least one model from <a href="/models.cgi" class="text-brand">My Models &rarr; Upload new model</a> before building a bundle.</p>
138 [/if]
139 [if:$has_candidates]
140 <input type="search" id="bunFilter" class="input" placeholder="Filter your listings..." style="margin-bottom:10px">
141 <div class="picker-grid" id="bunPicker">
142 [loop:@candidates]
143 <label class="picker-card" data-id="$loop1.id" data-title="$loop1.title">
144 <input type="checkbox" value="$loop1.id">
145 <span class="picker-title">$loop1.title</span>
146 <span class="picker-price">$loop1.price</span>
147 </label>
148 [/loop]
149 </div>
150 <p class="text-xs text-dim" style="margin-top:8px"><span id="bunPickedCount">0</span> selected</p>
151 [/if]
152 </div>
153 </div>
154
155 </div>
156
157 <div style="display:flex;flex-direction:column;gap:16px">
158
159 <div class="module">
160 <div class="module-head"><div class="left">
161 <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"/></svg></div>
162 <div><div class="module-title">Kind &amp; price</div><div class="module-sub">Digital files, printed boxes, or both.</div></div>
163 </div></div>
164 <div class="module-body">
165 <div class="form-group">
166 <label class="form-label">Kind</label>
167 <select name="product_kind" class="select" id="bunKind">
168 <option value="digital" $kind_digital_sel>Digital download (zip of all files)</option>
169 <option value="physical" $kind_physical_sel>3D-printed box (you ship everything together)</option>
170 <option value="both" $kind_both_sel>Both (buyer picks)</option>
171 </select>
172 </div>
173 <div class="form-group" id="bunDigitalPriceWrap">
174 <label class="form-label">Digital price <span class="req">*</span></label>
175 <div class="up-price-input"><span class="currency">\$</span>
176 <input type="number" name="base_price" class="input" placeholder="0.00" min="0" step="0.01" value="$base_price"></div>
177 </div>
178 <div class="form-group" id="bunPhysicalPriceWrap" style="display:none">
179 <label class="form-label">Printed price <span class="req">*</span></label>
180 <div class="up-price-input"><span class="currency">\$</span>
181 <input type="number" name="physical_price" class="input" placeholder="0.00" min="0" step="0.01" value="$physical_price"></div>
182 </div>
183 </div>
184 </div>
185
186 <div class="module" id="bunPhysicalBox" style="display:none">
187 <div class="module-head"><div class="left">
188 <div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="15" height="8" rx="1"/><polygon points="16 11 22 11 22 17 16 17 16 11"/></svg></div>
189 <div><div class="module-title">Physical bundle details</div><div class="module-sub">Stock + shipping for the printed box.</div></div>
190 </div></div>
191 <div class="module-body">
192 <div class="dash-grid-2" style="gap:10px">
193 <div class="form-group">
194 <label class="form-label">Stock</label>
195 <input type="number" name="inventory_qty" class="input" min="0" placeholder="Blank = unlimited" value="$inventory_qty">
196 </div>
197 <div class="form-group">
198 <label class="form-label">Production days</label>
199 <input type="number" name="production_time_days" class="input" min="0" placeholder="e.g. 7" value="$production_time_days">
200 </div>
201 <div class="form-group">
202 <label class="form-label">Weight (g)</label>
203 <input type="number" name="weight_grams" class="input" min="0" value="$weight_grams">
204 </div>
205 <div class="form-group">
206 <label class="form-label">Ships from</label>
207 <input type="text" name="ship_from_country" class="input" maxlength="2" placeholder="US" value="$ship_from_country" style="text-transform:uppercase">
208 </div>
209 </div>
210 <div class="form-group">
211 <label class="form-label">Shipping rates</label>
212 <textarea name="shipping_options_json" class="textarea" rows="3" placeholder="domestic=900&#10;worldwide=2200" style="font-family:var(--font-mono,monospace);font-size:12px">$shipping_options_json</textarea>
213 </div>
214 </div>
215 </div>
216
217 <div class="module">
218 <div class="module-body" style="display:flex;flex-direction:column;gap:8px">
219 <button type="submit" class="btn btn-primary" style="width:100%">Save bundle</button>
220 <a href="/bundles.cgi" class="btn btn-secondary" style="width:100%;text-align:center">Back to list</a>
221 </div>
222 </div>
223
224 </div>
225 </div>
226</form>
227
228<script>
229(function(){
230 /* Reveal the picker rows that match a filter substring. */
231 var filter = document.getElementById('bunFilter');
232 var picker = document.getElementById('bunPicker');
233 if (filter && picker) {
234 filter.addEventListener('input', function(){
235 var needle = filter.value.toLowerCase();
236 picker.querySelectorAll('.picker-card').forEach(function(card){
237 var hit = card.dataset.title.toLowerCase().indexOf(needle) !== -1;
238 card.style.display = hit ? '' : 'none';
239 });
240 });
241 }
242
243 /* Track selections, mirror to hidden field for submit. Initialize from
244 the comma-separated server-rendered selected_ids. */
245 var initial = ('$selected_ids' || '').split(',').filter(Boolean);
246 var hidden = document.getElementById('bunItemIds');
247 var countEl = document.getElementById('bunPickedCount');
248 var selected = {};
249 initial.forEach(function(id){ selected[id] = true; });
250 function sync() {
251 if (picker) picker.querySelectorAll('.picker-card').forEach(function(card){
252 var on = !!selected[card.dataset.id];
253 card.classList.toggle('is-on', on);
254 var chk = card.querySelector('input');
255 if (chk) chk.checked = on;
256 });
257 if (hidden) hidden.value = Object.keys(selected).join(',');
258 if (countEl) countEl.textContent = Object.keys(selected).length;
259 }
260 if (picker) {
261 picker.addEventListener('click', function(e){
262 var card = e.target.closest('.picker-card');
263 if (!card) return;
264 e.preventDefault();
265 var id = card.dataset.id;
266 if (selected[id]) delete selected[id]; else selected[id] = true;
267 sync();
268 });
269 }
270 sync();
271
272 /* Kind switch: show/hide digital + physical price + physical details box. */
273 var kindSel = document.getElementById('bunKind');
274 var digWrap = document.getElementById('bunDigitalPriceWrap');
275 var phyWrap = document.getElementById('bunPhysicalPriceWrap');
276 var phyBox = document.getElementById('bunPhysicalBox');
277 function applyKind() {
278 var k = kindSel.value;
279 digWrap.style.display = (k === 'physical') ? 'none' : '';
280 phyWrap.style.display = (k === 'digital') ? 'none' : '';
281 phyBox.style.display = (k === 'digital') ? 'none' : '';
282 }
283 if (kindSel) {
284 kindSel.addEventListener('change', applyKind);
285 applyKind();
286 }
287})();
288</script>
289[/if]
290
291<!-- trailing sentinel: MODS::Template needs non-conditional content at the end
292 of the file or the qq~~ eval returns empty for the whole page. -->
293<div style="display:none" data-end-of-bundles></div>
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help