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

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

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

Added
+286
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 7a457ae2273d
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 subscription plan manager. Backed by /subscription_plans.cgi. -->
2
3<div class="page-head">
4 <div>
5 <span class="page-eyebrow"><span class="dot"></span> Monetization &middot; Recurring</span>
6 <h1 class="page-title">Subscription plans</h1>
7 <p class="page-subtitle">Recurring revenue. Two flavors: <strong>download quota</strong> (buyer gets N STL downloads per month from a pool) or <strong>ship monthly</strong> (you ship them the newest item each month). Plans publish to your storefront with a Subscribe button.</p>
8 </div>
9 [if:!$is_editing]
10 <div class="page-actions">
11 <a href="/subscription_plans.cgi?id=new" class="btn btn-primary">
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 plan
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 .sp-list { display:grid; grid-template-columns:repeat(auto-fill, minmax(320px, 1fr)); gap:14px; }
28 .sp-card { padding:16px 18px; background:var(--col-surface-1); border:1px solid var(--col-border); border-radius:12px; display:flex; flex-direction:column; gap:8px; }
29 .sp-card .title { font-size:17px; font-weight:700; color:var(--col-text); }
30 .sp-card .price { font-family:var(--font-mono,monospace); font-weight:700; font-size:22px; color:var(--col-accent-bright); }
31 .sp-meta { font-size:12px; color:var(--col-text-3); }
32 .sp-pill { font-size:10px; padding:3px 9px; border-radius:5px; letter-spacing:1px; font-weight:700; text-transform:uppercase; }
33 .sp-pill.draft { background:rgba(156,163,175,0.18); color:#9ca3af; }
34 .sp-pill.active { background:rgba(34,197,94,0.18); color:#86efac; }
35 .sp-pill.paused { background:rgba(251,191,36,0.18); color:#fcd34d; }
36 .sp-pill.archived { background:rgba(239,68,68,0.18); color:#fca5a5; }
37 .sp-actions { display:flex; gap:6px; margin-top:auto; }
38 .sp-empty { padding:60px 24px; text-align:center; border:1px dashed var(--col-border-2); border-radius:14px; color:var(--col-text-2); }
39 .picker-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(220px, 1fr)); gap:8px; max-height:320px; overflow-y:auto; padding:6px; border:1px solid var(--col-border); border-radius:10px; background:var(--col-surface-2); }
40 .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; }
41 .picker-card.is-on { border-color:var(--col-accent); background:var(--grad-brand-soft); }
42 .picker-title { flex:1; font-size:12px; font-weight:600; color:var(--col-text); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
43</style>
44
45[if:!$is_editing]
46[if:!$has_plans]
47<div class="sp-empty">
48 <div style="font-size:48px;margin-bottom:8px">~</div>
49 <h2 style="color:#fff;font-family:var(--font-display,inherit);margin-bottom:8px">No subscription plans yet</h2>
50 <p>Offer recurring deals: <strong>"5 downloads/month from my Miniatures collection"</strong> or <strong>"I ship you my newest print each month"</strong>. Buyers see a Subscribe button on your storefront.</p>
51 <a href="/subscription_plans.cgi?id=new" class="btn btn-primary" style="margin-top:14px">Create your first plan</a>
52</div>
53[/if]
54
55[if:$has_plans]
56<div class="sp-list">
57 [loop:@plans]
58 <div class="sp-card">
59 <div style="display:flex;align-items:center;gap:8px">
60 <span class="sp-pill $loop1.status">$loop1.status</span>
61 <span style="font-size:11px;color:var(--col-text-3);text-transform:uppercase;letter-spacing:1px">$loop1.kind_label</span>
62 </div>
63 <div class="title">$loop1.name</div>
64 <div class="sp-meta">$loop1.tagline</div>
65 <div class="price">$loop1.price_label <span style="font-size:12px;font-weight:400;color:var(--col-text-3)">/ $loop1.cadence days</span></div>
66 <div class="sp-meta">$loop1.quota items per period &middot; $loop1.active_subs active subscriber(s)</div>
67 <div class="sp-actions">
68 <a href="/subscription_plans.cgi?id=$loop1.id" class="btn btn-secondary btn-sm">Edit</a>
69 [if:$loop1.is_draft]
70 <form method="POST" action="/subscription_plans.cgi" style="display:inline">
71 <input type="hidden" name="_act" value="publish"><input type="hidden" name="id" value="$loop1.id">
72 <button class="btn btn-primary btn-sm" type="submit">Publish</button>
73 </form>
74 [/if]
75 [if:$loop1.is_active]
76 <form method="POST" action="/subscription_plans.cgi" style="display:inline">
77 <input type="hidden" name="_act" value="pause"><input type="hidden" name="id" value="$loop1.id">
78 <button class="btn btn-secondary btn-sm" type="submit">Pause</button>
79 </form>
80 [/if]
81 [if:$loop1.is_paused]
82 <form method="POST" action="/subscription_plans.cgi" style="display:inline">
83 <input type="hidden" name="_act" value="publish"><input type="hidden" name="id" value="$loop1.id">
84 <button class="btn btn-primary btn-sm" type="submit">Resume</button>
85 </form>
86 [/if]
87 </div>
88 </div>
89 [/loop]
90</div>
91[/if]
92[/if]
93
94[if:$is_editing]
95<form method="POST" action="/subscription_plans.cgi">
96 [if:$is_new_form]<input type="hidden" name="_act" value="create">[/if]
97 [if:!$is_new_form]<input type="hidden" name="_act" value="update">[/if]
98 [if:!$is_new_form]<input type="hidden" name="id" value="$edit_id">[/if]
99 <input type="hidden" name="model_ids" id="spModelIds" value="">
100
101 <div class="dash-grid" style="grid-template-columns:2fr 1fr;gap:16px">
102 <div style="display:flex;flex-direction:column;gap:16px">
103
104 <div class="module">
105 <div class="module-head"><div class="left">
106 <div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M16 12a4 4 0 1 1-8 0"/></svg></div>
107 <div><div class="module-title">Plan basics</div><div class="module-sub">What buyers see on the Subscribe card.</div></div>
108 </div></div>
109 <div class="module-body">
110 <div class="form-group">
111 <label class="form-label">Name <span class="req">*</span></label>
112 <input type="text" name="name" class="input" required maxlength="160" placeholder="e.g. Miniatures Monthly" value="$name">
113 </div>
114 <div class="form-group">
115 <label class="form-label">Tagline</label>
116 <input type="text" name="tagline" class="input" maxlength="255" placeholder="One-liner shown under the name" value="$tagline">
117 </div>
118 <div class="form-group">
119 <label class="form-label">Description</label>
120 <textarea name="description" class="textarea" rows="5" placeholder="Sell the plan. What's included? When are new items added?">$description</textarea>
121 </div>
122 </div>
123 </div>
124
125 <div class="module">
126 <div class="module-head"><div class="left">
127 <div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg></div>
128 <div><div class="module-title">Item pool</div><div class="module-sub">Which of your listings count toward the subscriber's quota.</div></div>
129 </div></div>
130 <div class="module-body">
131 <div class="form-group">
132 <label class="form-label">Pool</label>
133 <select name="pool_kind" id="spPoolKind" class="select">
134 <option value="all" $pool_all_sel>All published listings on this storefront</option>
135 <option value="category" $pool_cat_sel>One of my categories</option>
136 <option value="specific" $pool_spec_sel>A specific list I curate</option>
137 </select>
138 </div>
139 <div class="form-group" id="spPoolCatWrap" style="display:none">
140 [if:$has_cat_options]
141 <label class="form-label">Category</label>
142 <select name="pool_category_id" class="select">
143 <option value="">&mdash; pick a category &mdash;</option>
144 [loop:@cat_options]
145 <option value="$loop1.id" $loop1.sel>$loop1.name</option>
146 [/loop]
147 </select>
148 [/if]
149 [if:!$has_cat_options]
150 <p class="text-xs text-dim">No categories yet. <a href="/categories.cgi" target="_blank" class="text-brand">Build one &rarr;</a></p>
151 [/if]
152 </div>
153 <div class="form-group" id="spPoolSpecWrap" style="display:none">
154 [if:!$has_candidates]
155 <p class="text-xs text-dim">You have no listings yet. <a href="/upload_model.cgi" class="text-brand">Upload one &rarr;</a></p>
156 [/if]
157 [if:$has_candidates]
158 <label class="form-label">Pick listings the subscriber can download from</label>
159 <input type="search" id="spFilter" class="input" placeholder="Filter listings..." style="margin-bottom:10px">
160 <div class="picker-grid" id="spPicker">
161 [loop:@candidates]
162 <label class="picker-card" data-id="$loop1.id" data-title="$loop1.title">
163 <input type="checkbox" value="$loop1.id" style="display:none">
164 <span class="picker-title">$loop1.title</span>
165 </label>
166 [/loop]
167 </div>
168 <p class="text-xs text-dim" style="margin-top:8px"><span id="spPickedCount">0</span> selected</p>
169 [/if]
170 </div>
171 </div>
172 </div>
173
174 </div>
175
176 <div style="display:flex;flex-direction:column;gap:16px">
177 <div class="module">
178 <div class="module-head"><div class="left">
179 <div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>
180 <div><div class="module-title">Cadence &amp; price</div><div class="module-sub">How often + how much.</div></div>
181 </div></div>
182 <div class="module-body">
183 <div class="form-group">
184 <label class="form-label">Kind <span class="req">*</span></label>
185 <select name="kind" class="select" id="spKind">
186 <option value="download_quota" $kind_dl_sel>Download quota (digital files)</option>
187 <option value="ship_monthly" $kind_ship_sel>Ship monthly (printed items)</option>
188 </select>
189 </div>
190 <div class="form-group">
191 <label class="form-label">Price per period <span class="req">*</span></label>
192 <div class="up-price-input"><span class="currency">\$</span>
193 <input type="number" name="price" class="input" required min="0.01" step="0.01" placeholder="9.00" value="$price"></div>
194 </div>
195 <div class="dash-grid-2" style="gap:10px">
196 <div class="form-group">
197 <label class="form-label">Cadence (days)</label>
198 <input type="number" name="cadence_days" class="input" min="1" value="$cadence_days">
199 </div>
200 <div class="form-group">
201 <label class="form-label" id="spQuotaLabel">Quota / period</label>
202 <input type="number" name="quota_per_period" class="input" min="1" value="$quota_per_period">
203 </div>
204 </div>
205 <div class="form-group">
206 <label class="form-label">Max subscribers <span class="text-xs text-dim">(0 = unlimited)</span></label>
207 <input type="number" name="max_subscribers" class="input" min="0" value="$max_subscribers">
208 </div>
209 </div>
210 </div>
211
212 <div class="module">
213 <div class="module-body" style="display:flex;flex-direction:column;gap:8px">
214 <button type="submit" class="btn btn-primary" style="width:100%">Save plan</button>
215 <a href="/subscription_plans.cgi" class="btn btn-secondary" style="width:100%;text-align:center">Back to list</a>
216 <p class="text-xs text-dim" style="margin-top:6px;line-height:1.5">After saving, click <strong>Publish</strong> from the list page to make the plan live. Buyers see a Subscribe button on your storefront.</p>
217 </div>
218 </div>
219 </div>
220 </div>
221</form>
222
223<script>
224(function(){
225 var poolSel = document.getElementById('spPoolKind');
226 var catWrap = document.getElementById('spPoolCatWrap');
227 var specWrap= document.getElementById('spPoolSpecWrap');
228 var kindSel = document.getElementById('spKind');
229 var quotaLbl= document.getElementById('spQuotaLabel');
230 function applyPool() {
231 var p = poolSel.value;
232 catWrap.style.display = (p === 'category') ? '' : 'none';
233 specWrap.style.display = (p === 'specific') ? '' : 'none';
234 }
235 function applyKind() {
236 quotaLbl.textContent = (kindSel.value === 'ship_monthly')
237 ? 'Items shipped / period' : 'Downloads / period';
238 }
239 if (poolSel) { poolSel.addEventListener('change', applyPool); applyPool(); }
240 if (kindSel) { kindSel.addEventListener('change', applyKind); applyKind(); }
241
242 // Specific-pool picker
243 var picker = document.getElementById('spPicker');
244 var filter = document.getElementById('spFilter');
245 var hidden = document.getElementById('spModelIds');
246 var countEl= document.getElementById('spPickedCount');
247 var initial= ('$selected_model_ids' || '').split(',').filter(Boolean);
248 var selected = {};
249 initial.forEach(function(id){ selected[id] = true; });
250 function sync() {
251 if (!picker) return;
252 picker.querySelectorAll('.picker-card').forEach(function(c){
253 var on = !!selected[c.dataset.id];
254 c.classList.toggle('is-on', on);
255 var chk = c.querySelector('input');
256 if (chk) chk.checked = on;
257 });
258 if (hidden) hidden.value = Object.keys(selected).join(',');
259 if (countEl) countEl.textContent = Object.keys(selected).length;
260 }
261 if (picker) {
262 picker.addEventListener('click', function(e){
263 var c = e.target.closest('.picker-card');
264 if (!c) return;
265 e.preventDefault();
266 var id = c.dataset.id;
267 if (selected[id]) delete selected[id]; else selected[id] = true;
268 sync();
269 });
270 sync();
271 }
272 if (filter && picker) {
273 filter.addEventListener('input', function(){
274 var n = filter.value.toLowerCase();
275 picker.querySelectorAll('.picker-card').forEach(function(c){
276 c.style.display = c.dataset.title.toLowerCase().indexOf(n) !== -1 ? '' : 'none';
277 });
278 });
279 }
280})();
281</script>
282[/if]
283
284<!-- trailing sentinel: MODS::Template silently returns empty if the
285 file ends on a conditional. Keeps the page body non-empty. -->
286<div style="display:none" data-end-of-plans></div>
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help