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

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

added on WebSTLs (webstls.com) at 2026-07-01 22:27:04

Added
+419
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 22e625775fb7
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 Seller Promotions.
3 - List view: KPI tiles + table of every promotion
4 - New view: create form
5 - Edit view: same form, prefilled
6 Posts to /promotion_action.cgi for create/update/delete/activate/pause.
7 Styles live in site.css under the .pr-* block (shared with the
8 admin promotions template).
9===================================================================== -->
10
11<div class="page-head">
12 <div>
13 <span class="page-eyebrow"><span class="dot"></span> Store &middot; Promotions</span>
14 <h1 class="page-title">Coupons, sales &amp; bundles</h1>
15 <p class="page-subtitle">Create coupon codes, time-limited sales, bundles, and shareable promo links for your store. Cap redemptions by count or by date, or both.</p>
16 </div>
17 <div class="page-actions">
18 [if:$is_list][if:$not_tutorial_mode]<a href="/promotions.cgi?tutorial=1" class="btn btn-secondary"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>Sample Data View</a>[/if][/if]
19 [if:$is_list][if:$tutorial_mode]<a href="/promotions.cgi" class="btn btn-secondary"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>Exit sample view</a>[/if][/if]
20 [if:$is_list]<a href="/promotions.cgi?new=1" class="btn btn-primary"><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>New promotion</a>[/if]
21 [if:$is_new]<a href="/promotions.cgi" class="btn btn-secondary">Cancel</a>[/if]
22 [if:$is_edit]<a href="/promotions.cgi" class="btn btn-secondary">Back to list</a>[/if]
23 </div>
24</div>
25
26[if:$tutorial_mode][if:$is_list]
27<div class="tutorial-banner">
28 <div class="tutorial-banner-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>
29 <div class="tutorial-banner-body">
30 <div class="tutorial-banner-title">Tutorial &middot; Sample data only</div>
31 <div class="tutorial-banner-text">The five promotions below are examples to show every status (active, scheduled, expired) and every kind (coupon, bundle, flash sale). None of them are real on your store.</div>
32 </div>
33 <a href="/promotions.cgi" class="btn btn-secondary btn-sm">Exit sample view</a>
34</div>
35
36<div class="tutorial-intro">
37 <div class="tutorial-intro-eyebrow">A walkthrough of Promotions</div>
38 <h2 class="tutorial-intro-title">Build coupon codes, sales, and bundles for your store.</h2>
39 <p class="tutorial-intro-text">Four kinds of promotion: <strong>Coupons</strong> need a code at checkout, <strong>Sales</strong> auto-apply during the window, <strong>Bundles</strong> require specific products together for a bundle price, and <strong>Flash sales</strong> behave like sales but checkout treats them as urgency-priced. Each promo can have a start/end window, a redemption cap (total uses OR per-user), a custom <code>/promo/&lt;slug&gt;</code> public link, and an optional new-customers-only filter.</p>
40 <p class="tutorial-intro-text">When a buyer enters a code at checkout or lands on a slug link, the promo is validated against its time window, redemption cap, minimum order, and customer-filter rules, then applied to the cart. Successful redemptions are written to <code>promotion_redemptions</code> so the listing table can show <em>87 of 500 used</em> and your analytics can attribute revenue.</p>
41 <p class="tutorial-intro-text tutorial-intro-foot">Click <a href="/promotions.cgi" class="text-brand fw-700">Exit sample view</a> to see your real promotions, or <a href="/promotions.cgi?new=1" class="text-brand fw-700">create your first one</a>.</p>
42</div>
43[/if][/if]
44
45[if:$has_flash]
46<div class="pr-flash $flash_kind">$flash_msg</div>
47[/if]
48
49[if:$schema_missing]
50<div class="an-banner" style="background:rgba(245,158,11,0.10);border-color:rgba(245,158,11,0.30)">
51 <strong>Promotions schema not yet installed.</strong> Run the latest <code>db_schema.sql</code> against your database to bring the promotion tables online.
52</div>
53[/if]
54
55[if:$is_list][if:$schema_ready]
56<div class="pr-kpis">
57 <div class="pr-kpi is-active">
58 <div class="lbl">Active</div>
59 <div class="val">$cnt_active</div>
60 </div>
61 <div class="pr-kpi is-scheduled">
62 <div class="lbl">Scheduled</div>
63 <div class="val">$cnt_scheduled</div>
64 </div>
65 <div class="pr-kpi is-paused">
66 <div class="lbl">Paused</div>
67 <div class="val">$cnt_paused</div>
68 </div>
69 <div class="pr-kpi is-expired">
70 <div class="lbl">Expired</div>
71 <div class="val">$cnt_expired</div>
72 </div>
73</div>
74
75[if:$has_rows]
76<table class="pr-table">
77 <thead>
78 <tr>
79 <th>Promotion</th>
80 <th>Kind</th>
81 <th>Discount</th>
82 <th>Window</th>
83 <th>Usage</th>
84 <th class="num">Discount given</th>
85 <th>Status</th>
86 <th>Actions</th>
87 </tr>
88 </thead>
89 <tbody>
90 [loop:@rows]
91 <tr>
92 <td>
93 <div class="name">$loop1.name</div>
94 <div class="meta">
95 [if:$loop1.code_present]Code: <code>$loop1.code</code>[/if]
96 [if:$loop1.slug_present][if:$loop1.code_present] &middot; [/if]Link: <code>/promo/$loop1.public_slug</code>[/if]
97 </div>
98 </td>
99 <td>
100 [if:$loop1.is_coupon]<span class="pill kind">Coupon</span>[/if]
101 [if:$loop1.is_sale]<span class="pill kind sale">Sale</span>[/if]
102 [if:$loop1.is_bundle]<span class="pill kind bundle">Bundle</span>[/if]
103 [if:$loop1.is_flash]<span class="pill kind flash">Flash sale</span>[/if]
104 </td>
105 <td>$loop1.discount_label</td>
106 <td>
107 [if:$loop1.has_window]
108 <div class="meta" style="font-size:12px;color:var(--col-text-2)">
109 $loop1.starts_at <br>&rarr; $loop1.ends_at
110 </div>
111 [/if]
112 [if:!$loop1.has_window]<span class="text-xs text-dim">No time limit</span>[/if]
113 </td>
114 <td>$loop1.cap_label</td>
115 <td class="num">$loop1.discount_given_display</td>
116 <td>
117 [if:$loop1.is_active]<span class="pill active">Active</span>[/if]
118 [if:$loop1.is_paused]<span class="pill paused">Paused</span>[/if]
119 [if:$loop1.is_scheduled]<span class="pill scheduled">Scheduled</span>[/if]
120 [if:$loop1.is_expired]<span class="pill expired">Expired</span>[/if]
121 [if:$loop1.is_draft]<span class="pill draft">Draft</span>[/if]
122 </td>
123 <td class="actions">
124 <a href="$loop1.edit_url">Edit</a>
125 [if:$loop1.is_active]
126 <span class="sep">&middot;</span>
127 <form method="POST" action="/promotion_action.cgi"
128 data-confirm-title="Pause this promotion?"
129 data-confirm-message="It will stop applying immediately and can be resumed later."
130 data-confirm-label="Pause promotion"
131 data-confirm-style="warning">
132 <input type="hidden" name="act" value="pause">
133 <input type="hidden" name="id" value="$loop1.id">
134 <button type="submit">Pause</button>
135 </form>
136 [/if]
137 [if:$loop1.is_scheduled]
138 <span class="sep">&middot;</span>
139 <form method="POST" action="/promotion_action.cgi"
140 data-confirm-title="Pause this scheduled promotion?"
141 data-confirm-message="It will not start at the scheduled time until you reactivate it."
142 data-confirm-label="Pause promotion"
143 data-confirm-style="warning">
144 <input type="hidden" name="act" value="pause">
145 <input type="hidden" name="id" value="$loop1.id">
146 <button type="submit">Pause</button>
147 </form>
148 [/if]
149 [if:$loop1.is_expired]
150 <span class="sep">&middot;</span>
151 <form method="POST" action="/promotion_action.cgi"
152 data-confirm-title="Pause this expired promotion?"
153 data-confirm-message="It is already past its end date. Pausing locks it so it cannot be reactivated without an Edit."
154 data-confirm-label="Pause promotion"
155 data-confirm-style="warning">
156 <input type="hidden" name="act" value="pause">
157 <input type="hidden" name="id" value="$loop1.id">
158 <button type="submit">Pause</button>
159 </form>
160 [/if]
161 [if:$loop1.is_paused]
162 <span class="sep">&middot;</span>
163 <form method="POST" action="/promotion_action.cgi">
164 <input type="hidden" name="act" value="activate">
165 <input type="hidden" name="id" value="$loop1.id">
166 <button type="submit">Activate</button>
167 </form>
168 [/if]
169 [if:$loop1.is_draft]
170 <span class="sep">&middot;</span>
171 <form method="POST" action="/promotion_action.cgi">
172 <input type="hidden" name="act" value="activate">
173 <input type="hidden" name="id" value="$loop1.id">
174 <button type="submit">Activate</button>
175 </form>
176 [/if]
177 <span class="sep">&middot;</span>
178 <form method="POST" action="/promotion_action.cgi"
179 data-confirm-title="Delete this promotion permanently?"
180 data-confirm-message="Redemption history is also removed. This cannot be undone."
181 data-confirm-label="Delete promotion"
182 data-confirm-style="danger">
183 <input type="hidden" name="act" value="delete">
184 <input type="hidden" name="id" value="$loop1.id">
185 <button type="submit" style="color:#fca5a5">Delete</button>
186 </form>
187 </td>
188 </tr>
189 [/loop]
190 </tbody>
191</table>
192[/if]
193
194[if:!$has_rows]
195<div style="text-align:center;padding:60px 24px;background:var(--col-surface-1);border:1px solid var(--col-border);border-radius:12px">
196 <div class="text-sm fw-600" style="color:#fff;margin-bottom:6px">No promotions yet</div>
197 <div class="text-xs text-dim" style="margin-bottom:14px;max-width:360px;margin-left:auto;margin-right:auto">Create coupon codes, time-limited sales, bundles, or shareable promo links to drive sales.</div>
198 <a href="/promotions.cgi?new=1" class="btn btn-primary btn-sm">Create your first promotion</a>
199</div>
200[/if]
201[/if][/if]
202
203[if:$is_new][if:$schema_ready]
204<form class="pr-form" method="POST" action="/promotion_action.cgi">
205 <input type="hidden" name="act" value="create">
206 <div class="pr-form-grid">
207 <div class="pr-field full">
208 <label>Name</label>
209 <input type="text" name="name" required maxlength="160" placeholder="e.g. Spring 20% off">
210 <div class="hint">Internal label. Buyers do not see this.</div>
211 </div>
212
213 <div class="pr-field full">
214 <label>Description</label>
215 <textarea name="description" maxlength="500" placeholder="What this promo is for, who it targets, etc."></textarea>
216 </div>
217
218 <div class="pr-field full">
219 <label>Kind</label>
220 <div class="pr-radio-row">
221 <label class="pr-radio is-on"><input type="radio" name="kind" value="coupon" checked> Coupon (requires code)</label>
222 <label class="pr-radio"><input type="radio" name="kind" value="sale"> Sale (auto-applies)</label>
223 <label class="pr-radio"><input type="radio" name="kind" value="bundle"> Bundle</label>
224 <label class="pr-radio"><input type="radio" name="kind" value="flash_sale"> Flash sale</label>
225 </div>
226 </div>
227
228 <div class="pr-field">
229 <label>Discount type</label>
230 <select name="discount_type">
231 <option value="percent">Percent off</option>
232 <option value="fixed_amount">Fixed amount off</option>
233 <option value="bundle_price">Bundle price</option>
234 <option value="free">Free (100% off)</option>
235 </select>
236 </div>
237
238 <div class="pr-field">
239 <label>Discount value</label>
240 <div style="display:flex;gap:8px;align-items:center">
241 <input type="number" name="discount_value_pct" min="0" max="100" step="1" placeholder="0" style="width:80px">
242 <span class="hint">% off</span>
243 <span class="hint">or</span>
244 <input type="number" name="discount_value_amt" min="0" step="0.01" placeholder="0.00" style="width:100px">
245 <span class="hint">$ amount</span>
246 </div>
247 <div class="hint">For percent: enter 20 for 20%. For fixed/bundle: enter the dollar amount.</div>
248 </div>
249
250 <div class="pr-field">
251 <label>Coupon code (optional)</label>
252 <input type="text" name="code" maxlength="64" placeholder="SPRING20" style="text-transform:uppercase">
253 <div class="hint">Buyers enter this at checkout. Leave blank if the promo auto-applies.</div>
254 </div>
255
256 <div class="pr-field">
257 <label>Public slug (optional)</label>
258 <input type="text" name="public_slug" maxlength="64" placeholder="spring20" style="text-transform:lowercase">
259 <div class="hint">Becomes a shareable link: <code>webstls.com/promo/spring20</code></div>
260 </div>
261
262 <div class="pr-field">
263 <label>Starts at</label>
264 <input type="datetime-local" name="starts_at_local">
265 </div>
266
267 <div class="pr-field">
268 <label>Ends at</label>
269 <input type="datetime-local" name="ends_at_local">
270 </div>
271
272 <div class="pr-field">
273 <label>Max total redemptions</label>
274 <input type="number" name="max_redemptions" min="0" placeholder="Unlimited">
275 <div class="hint">Leave blank for unlimited.</div>
276 </div>
277
278 <div class="pr-field">
279 <label>Max per buyer</label>
280 <input type="number" name="max_per_user" min="1" value="1">
281 </div>
282
283 <div class="pr-field">
284 <label>Minimum cart total</label>
285 <input type="number" name="minimum_order" min="0" step="0.01" placeholder="0.00">
286 <div class="hint">Cart must exceed this before promo applies.</div>
287 </div>
288
289 <div class="pr-field">
290 <label>New customers only</label>
291 <label class="pr-radio" style="display:inline-flex"><input type="checkbox" name="new_customers_only" value="1"> Only buyers with zero prior orders</label>
292 </div>
293
294 [if:$has_user_models]
295 <div class="pr-field full">
296 <label>Apply to specific models (optional)</label>
297 <div class="pr-models-box">
298 [loop:@user_models]
299 <label class="pr-model-row"><input type="checkbox" name="model_ids" value="$loop1.id"> $loop1.title</label>
300 [/loop]
301 </div>
302 <div class="hint">Leave all unchecked for store-wide. For Bundle kind, check the products that must all be in the cart.</div>
303 </div>
304 [/if]
305 </div>
306
307 <div class="pr-actions">
308 <button type="submit" name="publish_now" value="1" class="btn btn-primary">Create &amp; activate</button>
309 <button type="submit" class="btn btn-secondary">Save as draft</button>
310 </div>
311</form>
312[/if][/if]
313
314[if:$is_edit][if:$schema_ready][if:$has_edit]
315<form class="pr-form" method="POST" action="/promotion_action.cgi">
316 <input type="hidden" name="act" value="update">
317 <input type="hidden" name="id" value="$edit.id">
318 <div class="pr-form-grid">
319 <div class="pr-field full">
320 <label>Name</label>
321 <input type="text" name="name" required maxlength="160" value="$edit.name">
322 </div>
323
324 <div class="pr-field full">
325 <label>Description</label>
326 <textarea name="description" maxlength="500">$edit.description</textarea>
327 </div>
328
329 <div class="pr-field full">
330 <label>Kind</label>
331 <div class="pr-radio-row">
332 <label class="pr-radio[if:$edit.is_coupon] is-on[/if]"><input type="radio" name="kind" value="coupon"[if:$edit.is_coupon] checked[/if]> Coupon</label>
333 <label class="pr-radio[if:$edit.is_sale] is-on[/if]"><input type="radio" name="kind" value="sale"[if:$edit.is_sale] checked[/if]> Sale</label>
334 <label class="pr-radio[if:$edit.is_bundle] is-on[/if]"><input type="radio" name="kind" value="bundle"[if:$edit.is_bundle] checked[/if]> Bundle</label>
335 <label class="pr-radio[if:$edit.is_flash] is-on[/if]"><input type="radio" name="kind" value="flash_sale"[if:$edit.is_flash] checked[/if]> Flash sale</label>
336 </div>
337 </div>
338
339 <div class="pr-field">
340 <label>Discount type</label>
341 <select name="discount_type">
342 <option value="percent"[if:$edit.is_dtype_pct] selected[/if]>Percent off</option>
343 <option value="fixed_amount"[if:$edit.is_dtype_amt] selected[/if]>Fixed amount off</option>
344 <option value="bundle_price"[if:$edit.is_dtype_bun] selected[/if]>Bundle price</option>
345 <option value="free"[if:$edit.is_dtype_free] selected[/if]>Free</option>
346 </select>
347 </div>
348
349 <div class="pr-field">
350 <label>Discount value</label>
351 <div style="display:flex;gap:8px;align-items:center">
352 <input type="number" name="discount_value_pct" min="0" max="100" step="1" value="$edit.discount_value_pct" style="width:80px">
353 <span class="hint">% off</span>
354 <span class="hint">or</span>
355 <input type="number" name="discount_value_amt" min="0" step="0.01" value="$edit.discount_value_amt" style="width:100px">
356 <span class="hint">\$ amount</span>
357 </div>
358 </div>
359
360 <div class="pr-field">
361 <label>Coupon code</label>
362 <input type="text" name="code" maxlength="64" value="$edit.code" style="text-transform:uppercase">
363 </div>
364
365 <div class="pr-field">
366 <label>Public slug</label>
367 <input type="text" name="public_slug" maxlength="64" value="$edit.public_slug" style="text-transform:lowercase">
368 </div>
369
370 <div class="pr-field">
371 <label>Starts at</label>
372 <input type="datetime-local" name="starts_at_local" value="$edit.starts_at_local">
373 </div>
374
375 <div class="pr-field">
376 <label>Ends at</label>
377 <input type="datetime-local" name="ends_at_local" value="$edit.ends_at_local">
378 </div>
379
380 <div class="pr-field">
381 <label>Max total redemptions</label>
382 <input type="number" name="max_redemptions" min="0" value="$edit.max_redemptions" placeholder="Unlimited">
383 </div>
384
385 <div class="pr-field">
386 <label>Max per buyer</label>
387 <input type="number" name="max_per_user" min="1" value="$edit.max_per_user">
388 </div>
389
390 <div class="pr-field">
391 <label>Minimum cart total</label>
392 <input type="number" name="minimum_order" min="0" step="0.01" value="$edit.minimum_order">
393 </div>
394
395 <div class="pr-field">
396 <label>New customers only</label>
397 <label class="pr-radio" style="display:inline-flex"><input type="checkbox" name="new_customers_only" value="1"[if:$edit.new_customers_only] checked[/if]> Only buyers with zero prior orders</label>
398 </div>
399
400 [if:$has_user_models]
401 <div class="pr-field full">
402 <label>Apply to specific models</label>
403 <div class="pr-models-box">
404 [loop:@user_models]
405 <label class="pr-model-row"><input type="checkbox" name="model_ids" value="$loop1.id"[if:$loop1.is_selected] checked[/if]> $loop1.title</label>
406 [/loop]
407 </div>
408 </div>
409 [/if]
410 </div>
411
412 <div class="pr-actions">
413 <button type="submit" class="btn btn-primary">Save changes</button>
414 <a href="/promotions.cgi" class="btn btn-secondary">Cancel</a>
415 </div>
416</form>
417[/if][/if][/if]
418
419<div class="adm-foot-spacer" data-end="promotions">&nbsp;</div>
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help