Diff -- /var/www/vhosts/3dshawn.com/repricer.3dshawn.com/TEMPLATES/repricer_marketplaces.html

O Operator
Diff

/var/www/vhosts/3dshawn.com/repricer.3dshawn.com/TEMPLATES/repricer_marketplaces.html

added on local at 2026-07-01 21:47:25

Added
+648
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 13351d19ec44
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 Marketplaces dashboard. Tile per supported platform with status,
3 working Connect / Manage / Disconnect controls.
4
5 Connect flow:
6 1. Click Connect on a tile -> the per-platform modal opens (one
7 modal per tile, pre-rendered, hidden until clicked).
8 2. Modal contains the credential form fields defined in
9 MODS::RePricer::Marketplaces (cred_fields per platform).
10 3. Submit POSTs back to /marketplaces.cgi action=connect; the
11 CGI persists to marketplace_accounts, then redirects back
12 here with ?ok=connected&slug=X for the success banner.
13
14 Per-platform real API push code is its own per-platform project --
15 this template only owns the credential-storage UX.
16===================================================================== -->
17
18<style>
19 .mp-card { display:flex; flex-direction:column; gap:14px; padding:20px; height:100%; }
20 .mp-card .mp-actions { display:flex; gap:8px; flex-wrap:wrap; margin-top:auto; padding-top:6px; border-top:1px solid var(--col-border); }
21 .mp-card .mp-actions .btn { flex:1; }
22 .mp-card .mp-actions .btn.mp-btn-ghost { flex:0 0 auto; }
23 .mp-handle { font-family: var(--font-mono, monospace); font-size: 11px; color: var(--col-text-3); letter-spacing: 0.5px; }
24 .mp-err-inline { font-size: 11px; color: #f87171; margin-top: 4px; line-height: 1.4; }
25
26 /* ===== Connect modal =================================================
27 Each platform gets its own pre-rendered modal so the cred fields
28 are correctly templated server-side -- no JS field-list to keep in
29 sync with the Perl spec. Opens via [data-mp-open], closes via
30 [data-mp-close] or backdrop click or ESC. ====================== */
31 .mp-modal {
32 position: fixed; inset: 0; z-index: 1000;
33 display: none;
34 background: rgba(3, 8, 31, 0.78);
35 backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
36 align-items: flex-start; justify-content: center;
37 padding: 48px 16px; overflow-y: auto;
38 }
39 .mp-modal.is-open { display: flex; }
40 .mp-modal-card {
41 max-width: 560px; width: 100%;
42 background: var(--col-surface-1, #0c1729);
43 border: 1px solid rgba(124, 58, 237, 0.35);
44 border-radius: 14px;
45 padding: 24px 26px 22px;
46 box-shadow: 0 30px 60px rgba(0, 0, 0, 0.6);
47 position: relative;
48 }
49 .mp-modal-close {
50 position: absolute; top: 14px; right: 14px;
51 background: rgba(124, 58, 237, 0.15);
52 border: 1px solid rgba(124, 58, 237, 0.5);
53 color: #fff; border-radius: 8px;
54 width: 32px; height: 32px; cursor: pointer;
55 font-size: 18px; line-height: 1;
56 display: grid; place-items: center;
57 }
58 .mp-modal-close:hover { background: rgba(124, 58, 237, 0.30); }
59 .mp-modal-head {
60 display: flex; align-items: center; gap: 14px; margin-bottom: 14px;
61 }
62 .mp-modal-mark {
63 width: 44px; height: 44px; flex-shrink: 0;
64 border-radius: 10px;
65 background: linear-gradient(130deg, var(--col-accent), var(--col-accent-bright));
66 display: grid; place-items: center;
67 color: #fff; font-weight: 800; font-size: 18px;
68 box-shadow: 0 8px 24px rgba(124, 58, 237, 0.35);
69 }
70 .mp-modal-title { font-family: var(--font-display, system-ui); font-size: 20px; font-weight: 700; color: #fff; line-height: 1.15; }
71 .mp-modal-sub { font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--col-text-3); font-weight: 700; margin-top: 2px; }
72
73 .mp-field { margin-bottom: 14px; }
74 .mp-field label { display: block; font-size: 12px; font-weight: 600; color: var(--col-text-2); margin-bottom: 5px; letter-spacing: 0.02em; }
75 .mp-field input {
76 width: 100%; box-sizing: border-box;
77 background: rgba(15, 23, 42, 0.55);
78 border: 1px solid var(--col-border);
79 border-radius: 8px;
80 color: #fff;
81 padding: 10px 12px;
82 font-size: 13px;
83 font-family: var(--font-mono, monospace);
84 }
85 .mp-field input:focus { outline: none; border-color: var(--col-accent, #0f766e); box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.18); }
86 .mp-field .mp-hint { font-size: 11px; color: var(--col-text-3); margin-top: 4px; line-height: 1.5; }
87 .mp-field .mp-hint a { color: #14b8a6; text-decoration: underline; }
88
89 .mp-modal-actions { display: flex; gap: 10px; margin-top: 18px; }
90 .mp-modal-actions .btn { flex: 1; }
91 .mp-modal-actions .btn-cancel { flex: 0 0 auto; }
92
93 .mp-modal-help {
94 font-size: 11.5px;
95 color: var(--col-text-3);
96 line-height: 1.55;
97 background: rgba(15, 23, 42, 0.55);
98 border: 1px solid var(--col-border);
99 border-radius: 8px;
100 padding: 10px 12px;
101 margin-bottom: 16px;
102 }
103 .mp-modal-help strong { color: #6ee7b7; }
104 .mp-modal-help code {
105 background: rgba(124, 58, 237, 0.10);
106 border: 1px solid rgba(124, 58, 237, 0.30);
107 border-radius: 4px;
108 padding: 1px 5px;
109 color: #6ee7b7;
110 font-family: var(--font-mono, monospace);
111 font-size: 11px;
112 }
113
114 /* Disconnect confirm modal -- same chrome, danger accent. */
115 .mp-modal.is-danger .mp-modal-card { border-color: rgba(239, 68, 68, 0.40); }
116 .mp-modal.is-danger .mp-modal-mark { background: linear-gradient(130deg, #b91c1c, #ef4444); box-shadow: 0 8px 24px rgba(239, 68, 68, 0.30); }
117 .mp-modal.is-danger .mp-modal-close { background: rgba(239, 68, 68, 0.15); border-color: rgba(239, 68, 68, 0.40); }
118</style>
119
120
121<div class="page-head">
122 <div>
123 <span class="page-eyebrow"><span class="dot"></span> Marketplaces</span>
124 <h1 class="page-title">Push to other marketplaces</h1>
125 <p class="page-subtitle">$tile_count platforms supported. Connect with credentials from each platform &mdash; we store them encrypted at rest and use them to push your listings out.</p>
126 </div>
127</div>
128
129<!-- Outcome banner from the POST-redirect-GET. Connect / disconnect /
130 errors all surface here. -->
131[if:$show_ok]
132[if:$ok_connected]
133<div class="banner" style="margin-bottom:18px;background:rgba(34,197,94,0.10);border:1px solid rgba(34,197,94,0.40);color:#4ade80;border-radius:10px;padding:12px 14px;display:flex;align-items:center;gap:10px">
134 <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M9 12l2 2 4-4"/><circle cx="12" cy="12" r="10"/></svg>
135 <div class="text-sm"><strong>$flag_name connected.</strong> You're ready to push listings to this platform.</div>
136</div>
137[/if]
138[if:$ok_disconnected]
139<div class="banner" style="margin-bottom:18px;background:rgba(5,150,105,0.10);border:1px solid rgba(5,150,105,0.40);color:#10b981;border-radius:10px;padding:12px 14px;display:flex;align-items:center;gap:10px">
140 <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
141 <div class="text-sm"><strong>$flag_name disconnected.</strong> Tokens cleared from your account.</div>
142</div>
143[/if]
144[if:$ok_imported]
145<div class="banner" id="import-banner" style="margin-bottom:18px;background:rgba(34,197,94,0.10);border:1px solid rgba(34,197,94,0.40);color:#4ade80;border-radius:10px;padding:12px 14px;display:flex;align-items:flex-start;gap:10px">
146 <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" style="flex-shrink:0;margin-top:2px"><path d="M9 12l2 2 4-4"/><circle cx="12" cy="12" r="10"/></svg>
147 <div class="text-sm" style="flex:1;line-height:1.5">
148 <strong>Imported $import_done[if:$import_has_total] of $import_total[/if] model(s) from $flag_name.</strong>[if:$import_has_dups] <span style="color:#14b8a6">$import_dups already imported previously &mdash; skipped to avoid duplicates.</span>[/if][if:$import_has_more] <span id="auto-continue-msg">Pulling more in a moment&hellip;</span>[/if][if:!$import_has_more] They&#39;re saved as drafts in <a href="/models.cgi" style="color:#4ade80;text-decoration:underline">My Models</a> &mdash; review and publish when ready.[/if]
149 [if:$import_has_more]<div class="text-xs" style="color:rgba(74,222,128,0.75);margin-top:4px;font-weight:500">&#9888; Please keep this tab open and don&#39;t refresh until you see <strong>$import_total of $import_total</strong>. We&#39;ll auto-pull one model at a time until done.</div>[/if]
150 </div>
151 [if:$import_has_more]
152 <button type="button" id="auto-continue-stop" style="background:rgba(239,68,68,0.15);border:1px solid rgba(239,68,68,0.40);color:#f87171;border-radius:6px;padding:4px 10px;font-size:11px;font-weight:600;cursor:pointer;flex-shrink:0">Stop</button>
153 [/if]
154</div>
155[/if]
156[/if]
157[if:$show_err]
158<div class="banner" style="margin-bottom:18px;background:rgba(239,68,68,0.10);border:1px solid rgba(239,68,68,0.40);color:#f87171;border-radius:10px;padding:12px 14px;display:flex;align-items:center;gap:10px">
159 <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
160 <div class="text-sm"><strong>Couldn't connect $flag_name.</strong> &mdash; $flag_err. Double-check the fields and try again.</div>
161</div>
162[/if]
163[if:$show_imperr]
164<div class="banner" style="margin-bottom:18px;background:rgba(239,68,68,0.10);border:1px solid rgba(239,68,68,0.40);color:#f87171;border-radius:10px;padding:12px 14px;display:flex;align-items:flex-start;gap:10px">
165 <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" style="flex-shrink:0;margin-top:2px"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
166 <div class="text-sm" style="line-height:1.5"><strong>Couldn't pull models from $flag_name.</strong> &mdash; $flag_imperr<br><span class="text-xs text-dim">Click <strong>Manage</strong> on the $flag_name tile below to update the stored value, then try the pull again.</span></div>
167</div>
168[/if]
169
170<div class="banner info" style="margin-bottom:24px">
171 <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"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
172 <div class="text-xs" style="line-height:1.6">
173 <strong>How this works:</strong> repricer.com is your home base &mdash; buyers there pay you direct, no marketplace cut.
174 Use this page to also list each design on the marketplaces where your audience already shops.
175 <strong>Connect a platform</strong> by pasting your account handle + API token / access key from that platform. We store them so future "Push to marketplace" actions can ship a listing out without re-typing.
176 </div>
177</div>
178
179<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:18px">
180 [loop:@tiles]
181 <div class="card mp-card">
182
183 <div style="display:flex;align-items:center;gap:14px">
184 <div class="brand-mark" style="background:linear-gradient(130deg, var(--col-accent), var(--col-accent-bright));width:44px;height:44px;font-size:18px;flex-shrink:0">$loop1.icon</div>
185 <div style="flex:1;min-width:0">
186 <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
187 <strong style="font-size:17px">$loop1.name</strong>
188 $loop1.status_pill
189 </div>
190 <div class="text-xs text-dim" style="margin-top:2px;letter-spacing:1.5px;text-transform:uppercase">$loop1.auth &middot; $loop1.fees</div>
191 </div>
192 </div>
193
194 <p class="text-sm text-secondary" style="margin:0;line-height:1.5">$loop1.tagline</p>
195
196 <div class="text-xs text-dim" style="line-height:1.5"><strong>Sells:</strong> $loop1.sells</div>
197
198 [if:$loop1.is_connected]
199 <div class="mp-handle">Connected as <strong style="color:#4ade80">$loop1.account_handle</strong></div>
200 [if:$loop1.is_import_auto]
201 <form method="post" action="/marketplaces.cgi" style="margin:0" onsubmit="this.querySelector('button').disabled=true;this.querySelector('button').textContent='Importing… (this can take a minute)';">
202 <input type="hidden" name="action" value="import_models">
203 <input type="hidden" name="slug" value="$loop1.slug">
204 <button type="submit" class="btn btn-secondary btn-sm" style="width:100%;background:rgba(15,118,110,0.15);border:1px solid rgba(15,118,110,0.45);color:#6ee7b7">&darr; Pull in your models</button>
205 </form>
206 [/if]
207 [if:$loop1.is_import_manual]
208 <button type="button" class="btn btn-secondary btn-sm" data-mp-open="$loop1.modal_id-paste" style="width:100%;background:rgba(15,118,110,0.15);border:1px solid rgba(15,118,110,0.45);color:#6ee7b7">&darr; Paste your model URLs</button>
209 [/if]
210 [/if]
211 [if:$loop1.is_error]
212 <div class="mp-err-inline">$loop1.last_error</div>
213 [/if]
214
215 <div class="mp-actions">
216 [if:$loop1.is_connected]
217 <button type="button" class="btn btn-secondary btn-sm" data-mp-open="$loop1.modal_id">Manage</button>
218 <button type="button" class="btn btn-secondary btn-sm" data-mp-open="$loop1.modal_id-dc" style="background:rgba(239,68,68,0.10);border-color:rgba(239,68,68,0.35);color:#f87171">Disconnect</button>
219 [/if]
220 [if:!$loop1.is_connected]
221 <button type="button" class="btn btn-primary btn-sm" data-mp-open="$loop1.modal_id">Connect</button>
222 [/if]
223 <a href="$loop1.signup_url" target="_blank" rel="noopener" class="btn btn-ghost btn-sm mp-btn-ghost" title="Open the signup page on $loop1.name">Sign up</a>
224 </div>
225
226 </div>
227 [/loop]
228</div>
229
230
231<!-- ===== Per-platform Connect / Manage modals =======================
232 Each platform gets one form modal with its cred_fields rendered
233 server-side. Hidden by default; opened via the buttons above. -->
234[loop:@tiles]
235<div class="mp-modal" id="$loop1.modal_id" role="dialog" aria-modal="true" aria-labelledby="$loop1.modal_id-title">
236 <div class="mp-modal-card">
237 <button type="button" class="mp-modal-close" data-mp-close aria-label="Close">&times;</button>
238 <div class="mp-modal-head">
239 <div class="mp-modal-mark">$loop1.icon</div>
240 <div>
241 <div class="mp-modal-sub">[if:$loop1.is_connected]Manage[/if][if:!$loop1.is_connected]Connect[/if] &middot; $loop1.auth</div>
242 <div class="mp-modal-title" id="$loop1.modal_id-title">$loop1.name</div>
243 </div>
244 </div>
245
246 <div class="mp-modal-help">
247 <strong>Where to find these:</strong> $loop1.intro_text
248 </div>
249
250 [if:$loop1.has_cred_fields]
251 <form method="post" action="/marketplaces.cgi" autocomplete="off">
252 <input type="hidden" name="action" value="connect">
253 <input type="hidden" name="slug" value="$loop1.slug">
254
255 [loop:@cred_fields]
256 <div class="mp-field">
257 <label for="$loop2.name-$loop1.slug">$loop2.label[if:$loop2.required] <span style="color:#f87171">*</span>[/if][if:$loop2.is_saved] <span style="background:rgba(34,197,94,0.15);color:#4ade80;border:1px solid rgba(34,197,94,0.40);border-radius:4px;padding:1px 6px;font-size:9px;font-weight:700;letter-spacing:1px;margin-left:6px">&#10003; STORED</span>[/if]</label>
258 [if:$loop2.secret]
259 <input id="$loop2.name-$loop1.slug" name="$loop2.name" type="password" [if:$loop2.required]required[/if] autocomplete="off" spellcheck="false" placeholder="[if:$loop2.is_saved]&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull; Already stored -- leave blank to keep[/if]" style="[if:$loop2.is_saved]border-color:rgba(34,197,94,0.35);background:rgba(34,197,94,0.04)[/if]">
260 [/if]
261 [if:!$loop2.secret]
262 <input id="$loop2.name-$loop1.slug" name="$loop2.name" type="$loop2.type" value="$loop2.value" [if:$loop2.required]required[/if] autocomplete="off" spellcheck="false" style="[if:$loop2.is_saved]border-color:rgba(34,197,94,0.35);background:rgba(34,197,94,0.04)[/if]">
263 [/if]
264 [if:$loop2.is_saved]<div class="mp-hint" style="color:#4ade80;margin-top:6px"><strong>&#10003; Already stored in our system.</strong> No need to re-enter unless you want to update or replace it. Push and pull will use this saved value automatically.</div>[/if]
265 [if:$loop2.hint]<div class="mp-hint">$loop2.hint</div>[/if]
266 </div>
267 [/loop]
268
269 <div class="mp-modal-actions">
270 <button type="submit" class="btn btn-primary">[if:$loop1.is_connected]Save changes[/if][if:!$loop1.is_connected]Connect $loop1.name[/if]</button>
271 <button type="button" class="btn btn-secondary btn-cancel" data-mp-close>Cancel</button>
272 </div>
273 </form>
274 [/if]
275 [if:!$loop1.has_cred_fields]
276 <div class="text-sm text-dim" style="padding:14px 0">No credential schema defined for this platform yet. Contact support to add it.</div>
277 <div class="mp-modal-actions"><button type="button" class="btn btn-secondary" data-mp-close>Close</button></div>
278 [/if]
279 </div>
280</div>
281
282[if:$loop1.is_connected]
283[if:$loop1.is_import_manual]
284<!-- Paste-import modal: shown for platforms that sit behind Cloudflare
285 bot protection (no server-side scrape possible). User pastes a
286 list of their model URLs, one per line, and we create drafts. -->
287<div class="mp-modal" id="$loop1.modal_id-paste" role="dialog" aria-modal="true" aria-labelledby="$loop1.modal_id-paste-title">
288 <div class="mp-modal-card">
289 <button type="button" class="mp-modal-close" data-mp-close aria-label="Close">&times;</button>
290 <div class="mp-modal-head">
291 <div class="mp-modal-mark">$loop1.icon</div>
292 <div>
293 <div class="mp-modal-sub">Paste import &middot; $loop1.name</div>
294 <div class="mp-modal-title" id="$loop1.modal_id-paste-title">Pull your $loop1.name models</div>
295 </div>
296 </div>
297 <div class="mp-modal-help">
298 <strong>Why this is manual:</strong> $loop1.name is behind Cloudflare bot protection so RePricer can't fetch your model list automatically. Open your $loop1.name profile in a new tab, copy the URL of each model you want to import, and paste them below &mdash; one per line. We'll create a draft for each. You can fill in titles, prices, and files later when you edit each draft.
299 </div>
300 <form method="post" action="/marketplaces.cgi" onsubmit="this.querySelector('button[type=submit]').disabled=true;this.querySelector('button[type=submit]').textContent='Importing…';">
301 <input type="hidden" name="action" value="import_models_manual">
302 <input type="hidden" name="slug" value="$loop1.slug">
303 <div class="mp-field">
304 <label for="$loop1.modal_id-paste-urls">Model URLs (one per line)</label>
305 <textarea id="$loop1.modal_id-paste-urls" name="urls" rows="10" style="width:100%;box-sizing:border-box;background:rgba(15,23,42,0.55);border:1px solid var(--col-border);border-radius:8px;color:#fff;padding:10px 12px;font-size:12px;font-family:var(--font-mono,monospace);resize:vertical" placeholder="https://thangs.com/3d-model/my-first-design&#10;https://thangs.com/3d-model/another-design&#10;https://thangs.com/m/12345"></textarea>
306 <div class="mp-hint">Lines starting with <code>#</code> are skipped (use them for notes). Blank lines OK. We auto-derive a draft title from the URL slug; you'll edit the rest in <a href="/models.cgi" style="color:#14b8a6">My Models</a>.</div>
307 </div>
308 <div class="mp-modal-actions">
309 <button type="submit" class="btn btn-primary">Import drafts</button>
310 <button type="button" class="btn btn-secondary btn-cancel" data-mp-close>Cancel</button>
311 </div>
312 </form>
313 </div>
314</div>
315[/if]
316<!-- Disconnect-confirm modal: separate from the manage modal so the
317 "Save changes" form can't be confused with the destructive action. -->
318<div class="mp-modal is-danger" id="$loop1.modal_id-dc" role="dialog" aria-modal="true" aria-labelledby="$loop1.modal_id-dc-title">
319 <div class="mp-modal-card">
320 <button type="button" class="mp-modal-close" data-mp-close aria-label="Close">&times;</button>
321 <div class="mp-modal-head">
322 <div class="mp-modal-mark">!</div>
323 <div>
324 <div class="mp-modal-sub" style="color:#fca5a5">Disconnect</div>
325 <div class="mp-modal-title" id="$loop1.modal_id-dc-title">Remove $loop1.name credentials?</div>
326 </div>
327 </div>
328 <div class="text-sm text-secondary" style="line-height:1.6;margin-bottom:18px">
329 We'll clear your stored access token + handle for <strong>$loop1.name</strong>. Future pushes to this platform will pause until you reconnect. Your listings on $loop1.name itself stay where they are &mdash; this only severs the link from RePricer.
330 </div>
331 <form method="post" action="/marketplaces.cgi">
332 <input type="hidden" name="action" value="disconnect">
333 <input type="hidden" name="slug" value="$loop1.slug">
334 <div class="mp-modal-actions">
335 <button type="submit" class="btn btn-primary" style="background:linear-gradient(135deg,#b91c1c,#ef4444);border:none">Yes, disconnect</button>
336 <button type="button" class="btn btn-secondary btn-cancel" data-mp-close>Cancel</button>
337 </div>
338 </form>
339 </div>
340</div>
341[/if]
342[/loop]
343
344
345<!-- ===== Manual export reminder ====================================== -->
346<div class="module" style="margin-top:32px">
347 <div class="module-head">
348 <div class="left">
349 <div class="module-icon">
350 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
351 </div>
352 <div>
353 <div class="module-title">Manual export (always available)</div>
354 <div class="module-sub">Even when a platform isn't connected, open any of your models and click "Export" to get a ready-to-paste listing for any platform.</div>
355 </div>
356 </div>
357 <a href="/models.cgi" class="btn btn-primary btn-sm">Open Models &rarr;</a>
358 </div>
359</div>
360
361
362<!-- ===== Auto-continue pull-import loop ================================
363 When the server returns more=1 in the success redirect, the
364 import had more models to fetch but stopped at the per-call
365 batch cap to stay under nginx's CGI timeout. This script auto-
366 resubmits the Pull form for the same platform after a short
367 delay, so the user clicks ONCE and we batch through the full
368 library in the background. Each iteration is a fresh HTTP
369 request, so we never hit the server-side timeout no matter how
370 big the library is.
371
372 The Stop button cancels the next-scheduled iteration. Already-
373 imported models stay imported (dedup is row-by-row, committed
374 as each completes). ================================================= -->
375<script>
376// ALWAYS-ON: clear the per-platform sessionStorage "stopped" flag
377// whenever the user manually submits a Pull form on ANY marketplaces
378// page (not just an in-progress one). Previously this listener was
379// nested inside the auto-loop IIFE below and only attached on
380// response pages with ?more=1 -- so clicking the Pull button from a
381// plain Marketplaces page left the flag set, the import did one
382// batch, redirected to a more=1 page, and the new auto-loop
383// immediately halted because the stale flag was still there.
384(function () {
385 document.addEventListener('submit', function (ev) {
386 var f = ev.target;
387 if (!f || f.tagName !== 'FORM') return;
388 if (f.getAttribute('action') !== '/marketplaces.cgi') return;
389 var actionIn = f.querySelector('input[name="action"]');
390 var slugIn = f.querySelector('input[name="slug"]');
391 if (!actionIn || !slugIn) return;
392 if (actionIn.value !== 'import_models' && actionIn.value !== 'import_models_manual') return;
393 try { sessionStorage.removeItem('mp_auto_stopped_' + slugIn.value); } catch (e) {}
394 }, true);
395})();
396
397// Helper: morph the Stop button into a Continue button. Used both
398// when the user clicks Stop (after the red "Stopping..." settle) and
399// when the page loads with the stopped flag still set in
400// sessionStorage from a previous tab session.
401function _mpMorphStopToContinue(btn, target, stopKey) {
402 if (!btn || !target) return null;
403 var clone = btn.cloneNode(true);
404 btn.parentNode.replaceChild(clone, btn);
405 clone.textContent = 'Continue';
406 // Match the topbar Help button styling -- blue glassy chip so it
407 // reads as a friendly action (not a destructive button). Same
408 // gradient / border / text colors as .help-btn in repricer_wrapper.
409 clone.style.background = 'linear-gradient(135deg, rgba(5,150,105,0.22) 0%, rgba(5,150,105,0.18) 100%)';
410 clone.style.border = '1px solid rgba(5,150,105,0.55)';
411 clone.style.color = '#6ee7b7';
412 clone.style.borderRadius = '10px';
413 clone.style.padding = '0 14px';
414 clone.style.height = '36px';
415 clone.style.fontWeight = '600';
416 clone.style.display = 'inline-flex';
417 clone.style.alignItems = 'center';
418 clone.style.gap = '7px';
419 clone.style.cursor = 'pointer';
420 clone.style.transition = 'background 0.15s, box-shadow 0.15s, color 0.15s';
421 clone.addEventListener('mouseenter', function () {
422 clone.style.background = 'linear-gradient(135deg, rgba(5,150,105,0.32) 0%, rgba(5,150,105,0.26) 100%)';
423 clone.style.color = '#a7f3d0';
424 clone.style.boxShadow = '0 0 18px rgba(5,150,105,0.35)';
425 });
426 clone.addEventListener('mouseleave', function () {
427 clone.style.background = 'linear-gradient(135deg, rgba(5,150,105,0.22) 0%, rgba(5,150,105,0.18) 100%)';
428 clone.style.color = '#6ee7b7';
429 clone.style.boxShadow = 'none';
430 });
431 clone.addEventListener('click', function () {
432 try { sessionStorage.removeItem(stopKey); } catch (e) {}
433 // Thread the running counts from the URL so the spinner page
434 // shows "Imported X of Y" instead of the cold-start text.
435 try {
436 var p = new URLSearchParams(window.location.search);
437 _mpThreadRunningCounts(target, p.get('done'), p.get('total'));
438 } catch (e) {}
439 target.submit();
440 });
441 return clone;
442}
443
444// Inject (or update) hidden running_done / running_total inputs on the
445// auto-import form so the spinner page (streaming response from the
446// next POST) can render its green banner with the correct count
447// instead of "Starting import" every iteration.
448function _mpThreadRunningCounts(form, done, total) {
449 if (!form) return;
450 function setHidden(name, value) {
451 var el = form.querySelector('input[type="hidden"][name="' + name + '"]');
452 if (!el) {
453 el = document.createElement('input');
454 el.type = 'hidden';
455 el.name = name;
456 form.appendChild(el);
457 }
458 el.value = value == null ? '' : String(value);
459 }
460 setHidden('running_done', done == null ? '' : done);
461 setHidden('running_total', total == null ? '' : total);
462}
463
464(function () {
465 var params = new URLSearchParams(window.location.search);
466 if (params.get('ok') !== 'imported') return;
467 if (params.get('more') !== '1') return;
468 var slug = params.get('slug') || '';
469 if (!slug) return;
470
471 var STOP_KEY = 'mp_auto_stopped_' + slug;
472
473 // Find the auto-import form for THIS platform (action=import_models
474 // + matching slug). There can be multiple connected platforms on
475 // the page, so we filter by slug to hit the right one.
476 var target = null;
477 var forms = document.querySelectorAll('form[action="/marketplaces.cgi"]');
478 for (var i = 0; i < forms.length; i++) {
479 var a = forms[i].querySelector('input[name="action"]');
480 var s = forms[i].querySelector('input[name="slug"]');
481 if (a && s && a.value === 'import_models' && s.value === slug) {
482 target = forms[i];
483 break;
484 }
485 }
486 if (!target) return;
487
488 // Honor a previous Stop click (sessionStorage flag survives page
489 // navigations). Render a static "Stopped" message + morph the Stop
490 // button into a Continue button so the user has an obvious way
491 // back without having to scroll to find the tile.
492 try {
493 if (window.sessionStorage && sessionStorage.getItem(STOP_KEY) === '1') {
494 var existing = document.getElementById('auto-continue-msg');
495 if (existing) {
496 existing.innerHTML = '<strong>Auto-pull stopped by you.</strong> Click <strong>Continue</strong> to resume.';
497 }
498 // Hide the "please keep tab open" warning -- doesn't apply here.
499 var banner = document.getElementById('import-banner');
500 var warn = banner ? banner.querySelector('.text-xs') : null;
501 if (warn) warn.style.display = 'none';
502 var stopBtnEl = document.getElementById('auto-continue-stop');
503 _mpMorphStopToContinue(stopBtnEl, target, STOP_KEY);
504 return;
505 }
506 } catch (e) { /* sessionStorage blocked; fall through */ }
507
508 // Visual countdown in the banner so the user knows what's happening.
509 var msg = document.getElementById('auto-continue-msg');
510 var stopBtn = document.getElementById('auto-continue-stop');
511 var seconds = 2;
512 var stopped = false;
513 var ticker = null;
514
515 function render() {
516 if (msg && !stopped) {
517 msg.textContent = 'Pulling next model in ' + seconds + 's…';
518 }
519 }
520 render();
521
522 ticker = setInterval(function () {
523 if (stopped) return;
524 seconds--;
525 render();
526 if (seconds <= 0) {
527 clearInterval(ticker);
528 if (!stopped) {
529 // Countdown finished -- switch to the "actively pulling"
530 // state with animated dots so the user can tell the
531 // request is in flight (the browser shows the previous
532 // page while waiting for the POST response, otherwise
533 // the banner would look frozen). Dots cycle . -> .. ->
534 // ... every 400ms. Browser navigates when the response
535 // lands, which kills this script naturally.
536 if (msg) {
537 var dotCount = 0;
538 setInterval(function () {
539 if (stopped) return;
540 dotCount = (dotCount % 3) + 1;
541 msg.textContent = 'Pulling next model now' + Array(dotCount + 1).join('.');
542 }, 400);
543 }
544 _mpThreadRunningCounts(target, params.get('done'), params.get('total'));
545 target.submit();
546 }
547 }
548 }, 1000);
549
550 if (stopBtn) {
551 stopBtn.addEventListener('click', function () {
552 stopped = true;
553 if (ticker) clearInterval(ticker);
554 // Persist the intent across the in-flight POST + page nav.
555 // Without this, the next page load would resume auto-pull.
556 try { sessionStorage.setItem(STOP_KEY, '1'); } catch (e) {}
557
558 // Immediate visual feedback: flip the banner red and show an
559 // animated "Stopping, please wait…" while we wait for any
560 // in-flight POST to finish. If a request was already mid-
561 // flight when Stop was clicked, we can't actually cancel it
562 // server-side -- but at least the user sees we acknowledged
563 // the click. After ~1.5s we settle back into the green
564 // "stopped" state (or the page navigates first and the
565 // sessionStorage flag takes over on the next page).
566 var banner = document.getElementById('import-banner');
567 if (banner) {
568 banner.style.background = 'rgba(239,68,68,0.14)';
569 banner.style.borderColor = 'rgba(239,68,68,0.55)';
570 banner.style.color = '#f87171';
571 }
572 // Hide the "please keep this tab open" line -- it doesn't
573 // apply once stop is requested.
574 var warnLine = banner ? banner.querySelector('.text-xs') : null;
575 if (warnLine) warnLine.style.display = 'none';
576
577 stopBtn.style.display = 'none';
578
579 // Animated dots so the user can see the click was received.
580 var stopDots = 1;
581 if (msg) { msg.textContent = 'Stopping, please wait.'; }
582 var stopDotTimer = setInterval(function () {
583 if (!msg) return;
584 stopDots = (stopDots % 3) + 1;
585 msg.textContent = 'Stopping, please wait' + Array(stopDots + 1).join('.');
586 }, 300);
587
588 // Settle into the green "Stopped" final state after a moment.
589 // Note: if a POST was in flight, the page may navigate before
590 // this fires -- that's fine, sessionStorage handles the next
591 // page's banner.
592 setTimeout(function () {
593 clearInterval(stopDotTimer);
594 if (banner) {
595 banner.style.background = '';
596 banner.style.borderColor = '';
597 banner.style.color = '';
598 }
599 if (msg) {
600 msg.innerHTML = '<strong>Stopped.</strong> Click <strong>Continue</strong> to resume.';
601 }
602 // Bring the button back, but now as Continue.
603 var continueBtn = _mpMorphStopToContinue(stopBtn, target, STOP_KEY);
604 }, 1500);
605 });
606 }
607})();
608</script>
609
610
611<!-- ===== Modal open/close wiring ===================================== -->
612<script>
613(function () {
614 // Open: any element with [data-mp-open="<modal-id>"].
615 document.addEventListener('click', function (e) {
616 var opener = e.target.closest ? e.target.closest('[data-mp-open]') : null;
617 if (opener) {
618 var id = opener.getAttribute('data-mp-open');
619 var m = id ? document.getElementById(id) : null;
620 if (m) {
621 m.classList.add('is-open');
622 // Focus the first text input so the seller can start typing.
623 var first = m.querySelector('input[type="text"], input[type="password"], input[type="url"]');
624 if (first) setTimeout(function () { first.focus(); }, 30);
625 }
626 e.preventDefault();
627 return;
628 }
629 // Close: any [data-mp-close], OR clicking the backdrop itself.
630 var closer = e.target.closest ? e.target.closest('[data-mp-close]') : null;
631 if (closer) {
632 var m2 = closer.closest('.mp-modal');
633 if (m2) m2.classList.remove('is-open');
634 e.preventDefault();
635 return;
636 }
637 if (e.target.classList && e.target.classList.contains('mp-modal')) {
638 e.target.classList.remove('is-open');
639 }
640 });
641 // ESC closes any currently-open modal.
642 document.addEventListener('keydown', function (e) {
643 if (e.key !== 'Escape') return;
644 var open = document.querySelectorAll('.mp-modal.is-open');
645 for (var i = 0; i < open.length; i++) open[i].classList.remove('is-open');
646 });
647})();
648</script>