← back to Sample Box
step 3: index.html — real Shopify catalog swatch grid + Shopify-hosted checkout
936c9a832f8386cddb5a49e0c33cce7ce639426b · 2026-07-16 07:37:35 -0700 · Steve Abrams
- Swatch grid now loads from /api/shopify-products (200 real products:
title, vendor, image, price) with search box, sort (6 options), and
density slider (2-6 cols) — all persisted to localStorage.
- "Skip — Browse All" button bypasses vibe picker.
- Stripe payment stub removed entirely; replaced with:
- TEST-MODE banner explaining Bogus Gateway + PayPal toggle note
- "Proceed to Secure Shopify Checkout" green CTA button
- On click: local order captured first (POST /api/orders), then
window.location → https://www.designerwallcoverings.com/cart/<variant>:<qty>
- Confirmation panel shows local order ID + fallback Shopify link in case
auto-redirect is blocked.
- House rules preserved: sort+density persist to localStorage, vibe filter
still works for legacy local products.json vibes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Files touched
Diff
commit 936c9a832f8386cddb5a49e0c33cce7ce639426b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 16 07:37:35 2026 -0700
step 3: index.html — real Shopify catalog swatch grid + Shopify-hosted checkout
- Swatch grid now loads from /api/shopify-products (200 real products:
title, vendor, image, price) with search box, sort (6 options), and
density slider (2-6 cols) — all persisted to localStorage.
- "Skip — Browse All" button bypasses vibe picker.
- Stripe payment stub removed entirely; replaced with:
- TEST-MODE banner explaining Bogus Gateway + PayPal toggle note
- "Proceed to Secure Shopify Checkout" green CTA button
- On click: local order captured first (POST /api/orders), then
window.location → https://www.designerwallcoverings.com/cart/<variant>:<qty>
- Confirmation panel shows local order ID + fallback Shopify link in case
auto-redirect is blocked.
- House rules preserved: sort+density persist to localStorage, vibe filter
still works for legacy local products.json vibes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---
public/index.html | 287 ++++++++++++++++++++++++++++++++++++++++++------------
1 file changed, 225 insertions(+), 62 deletions(-)
diff --git a/public/index.html b/public/index.html
index a2bba97..6d8f0d4 100644
--- a/public/index.html
+++ b/public/index.html
@@ -62,11 +62,19 @@ select,input[type=range]{font:inherit}
.form-group input:focus,.form-group select:focus{outline:none;border-color:var(--accent)}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
-.payment-stub{background:#fffbf0;border:2px dashed var(--gold);border-radius:var(--radius);padding:20px;margin:20px 0;text-align:center}
-.payment-stub .stub-label{font-size:11px;font-weight:700;letter-spacing:.08em;color:var(--gold);margin-bottom:8px}
-.payment-stub .stub-amount{font-size:28px;font-weight:700;color:var(--accent)}
-.payment-stub .stub-card{font-size:13px;color:var(--muted);margin:8px 0;font-family:monospace}
-.payment-stub .stub-note{font-size:11px;color:var(--muted);margin-top:6px}
+/* TEST-MODE banner */
+.test-mode-banner{background:#fff8e1;border:2px solid #f5a623;border-radius:var(--radius);padding:14px 18px;margin:20px 0;font-size:13px;line-height:1.5}
+.test-mode-banner strong{color:#b8730a;display:block;font-size:12px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;margin-bottom:4px}
+
+/* Shopify checkout CTA */
+.shopify-cta-wrap{background:#f0faf4;border:2px solid #27ae60;border-radius:var(--radius);padding:22px 20px;margin:20px 0;text-align:center}
+.shopify-cta-wrap .cta-label{font-size:12px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:#27ae60;margin-bottom:8px}
+.shopify-cta-wrap .cta-amount{font-size:26px;font-weight:700;color:var(--accent);margin-bottom:4px}
+.shopify-cta-wrap .cta-note{font-size:12px;color:var(--muted);margin-bottom:16px;line-height:1.5}
+.btn-shopify{display:inline-flex;align-items:center;gap:10px;padding:14px 32px;border-radius:7px;font:700 15px/1 inherit;cursor:pointer;transition:.15s;border:none;background:#27ae60;color:#fff;text-decoration:none;width:100%;justify-content:center}
+.btn-shopify:hover{background:#219150}
+.btn-shopify:disabled,.btn-shopify[aria-disabled=true]{opacity:.5;cursor:not-allowed;pointer-events:none}
+.shopify-lock{font-size:18px}
.btn{display:inline-flex;align-items:center;gap:8px;padding:13px 28px;border-radius:7px;font:600 15px/1 inherit;cursor:pointer;transition:.15s;border:none}
.btn-primary{background:var(--accent);color:#fff}
@@ -87,8 +95,12 @@ select,input[type=range]{font:inherit}
.confirm-detail p{font-size:13px;padding:5px 0;border-bottom:1px solid var(--border)}
.confirm-detail p:last-child{border-bottom:none}
.credit-chip{display:inline-block;background:#f0faf4;color:#27ae60;border:1px solid #27ae60;border-radius:5px;padding:6px 12px;font-size:13px;font-weight:600;margin-top:12px}
+.shopify-redirect-notice{background:#e8f5e9;border:1px solid #a5d6a7;border-radius:6px;padding:12px 16px;margin-top:16px;font-size:13px;color:#2e7d32}
.empty{padding:60px 20px;text-align:center;color:var(--muted)}
+/* search bar */
+.search-bar{padding:10px 14px;border:1px solid var(--border);border-radius:6px;font:inherit;font-size:14px;width:220px;background:#fff}
+
@media(max-width:640px){
.swatch-grid{--cols:2!important}
.vibe-grid{grid-template-columns:repeat(2,1fr)}
@@ -121,20 +133,27 @@ select,input[type=range]{font:inherit}
We'll curate a box of hand-picked real samples from our collection.
</p>
<div class="vibe-grid" id="vibe-grid"><div class="empty">Loading vibes…</div></div>
- <div class="btns" style="justify-content:center;margin-top:32px">
+ <div style="text-align:center;margin-top:20px">
+ <button class="btn btn-outline" id="btn-skip-vibe" style="font-size:13px;padding:10px 20px">
+ Skip — Browse All Real Products →
+ </button>
+ </div>
+ <div class="btns" style="justify-content:center;margin-top:16px">
<button class="btn btn-primary" id="btn-vibe-next" disabled>See My Swatches →</button>
</div>
</div>
-<!-- PANEL 2: SWATCH GRID -->
+<!-- PANEL 2: SWATCH GRID (real Shopify catalog) -->
<div class="panel" id="panel-swatches">
<div class="grid-toolbar">
<h2 id="swatch-heading">Swatches</h2>
+ <input class="search-bar" id="search" type="search" placeholder="Search title / vendor / SKU…">
<label class="ctrl">Sort
<select id="sort">
<option value="newest">Newest</option>
<option value="title">Title A→Z</option>
<option value="sku">SKU A→Z</option>
+ <option value="vendor">Vendor A→Z</option>
<option value="price-asc">Price ↑</option>
<option value="price-desc">Price ↓</option>
</select>
@@ -149,7 +168,7 @@ select,input[type=range]{font:inherit}
</div>
</div>
-<!-- PANEL 3: CHECKOUT -->
+<!-- PANEL 3: CHECKOUT (Shopify-hosted handoff) -->
<div class="panel" id="panel-checkout">
<div class="checkout-wrap">
<div class="order-summary" id="order-summary">
@@ -196,29 +215,56 @@ select,input[type=range]{font:inherit}
</select>
</div>
</div>
- <div class="payment-stub">
- <div class="stub-label">TEST CHECKOUT — NO REAL CHARGE</div>
- <div class="stub-amount" id="stub-amount">$15.00</div>
- <div class="stub-card">Card: 4242 4242 4242 4242 | 12/99 | CVC 123</div>
- <div class="stub-note">Stripe test mode stub. To wire real Stripe: set STRIPE_SECRET_KEY in .env,<br>call stripe.checkout.sessions.create() in server.js POST /api/checkout.</div>
+
+ <!-- TEST-MODE banner — replaces old Stripe stub -->
+ <div class="test-mode-banner">
+ <strong>⚠️ TEST — Shopify Live Checkout Handoff</strong>
+ Clicking "Proceed to Shopify Checkout" opens the <em>real</em>
+ <strong>www.designerwallcoverings.com</strong> checkout. Only complete a
+ purchase while the store has <strong>Bogus Gateway</strong> (Shopify
+ test-mode) active — otherwise a real charge will occur.
+ <br><br>
+ <strong>PayPal note:</strong> PayPal appears in Shopify checkout when
+ enabled under <em>Store Admin → Settings → Payments → Shopify Payments →
+ PayPal</em>. This is a store-admin toggle — not controlled by this app.
</div>
</form>
+
+ <!-- Shopify checkout CTA -->
+ <div class="shopify-cta-wrap">
+ <div class="cta-label">Secure Shopify Checkout</div>
+ <div class="cta-amount" id="cta-amount">$15.00</div>
+ <div class="cta-note" id="cta-note">
+ Each swatch = one $4.25 memo-sample. Box total $15.00 fully credited<br>
+ toward your first roll order.
+ </div>
+ <a id="btn-shopify-checkout" class="btn-shopify" href="#" aria-disabled="true"
+ onclick="return handleShopifyCheckout(event)">
+ <span class="shopify-lock">🔒</span>
+ Proceed to Secure Shopify Checkout
+ </a>
+ </div>
+
<div class="btns">
<button class="btn btn-outline" id="btn-back-swatches">← Back</button>
- <button class="btn btn-gold" id="btn-place-order">Place Test Order — $15.00</button>
</div>
<p id="order-error" style="color:#e74c3c;font-size:13px;margin-top:12px;display:none"></p>
</div>
</div>
-<!-- PANEL 4: CONFIRMATION -->
+<!-- PANEL 4: CONFIRMATION (local capture done — user redirected to Shopify) -->
<div class="panel" id="panel-confirm">
<div class="confirm-box">
<div class="tick">✅</div>
- <h2>Your Sample Box is Ordered!</h2>
+ <h2>Order Captured — Redirecting to Checkout</h2>
<div class="order-id" id="confirm-order-id"></div>
- <p style="font-size:14px;color:var(--muted)">We'll ship your curated swatch box within 3-5 business days.</p>
+ <p style="font-size:14px;color:var(--muted)">Your order details have been saved. Complete payment on the secure Shopify checkout.</p>
<span class="credit-chip" id="confirm-credit"></span>
+ <div class="shopify-redirect-notice" id="shopify-redirect-notice" style="display:none">
+ 🔗 <strong>Redirecting to Shopify checkout…</strong><br>
+ If the page doesn't redirect automatically,
+ <a id="shopify-fallback-link" href="#" target="_blank" style="color:#27ae60;font-weight:600">click here to complete checkout</a>.
+ </div>
<div class="confirm-detail" id="confirm-detail"></div>
<div class="btns" style="justify-content:center;margin-top:24px">
<button class="btn btn-outline" onclick="location.reload()">Order Another Box</button>
@@ -227,13 +273,14 @@ select,input[type=range]{font:inherit}
</div>
<script>
-const state={vibe:null,selected:[],order:null};
+// ── state ──────────────────────────────────────────────────────────────────────
+const state={vibe:null,selected:[],order:null,meta:{checkout_domain:'',sample_variant_id:''}};
const esc=s=>(s||'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
const $=s=>document.querySelector(s);
-const MAX=8,PRICE=15.00;
+const MAX=8, PRICE=15.00;
-// sort+density — house rule: persist to localStorage
-const sortSel=$('#sort'),dens=$('#density');
+// ── sort+density — house rule: persist to localStorage ─────────────────────────
+const sortSel=$('#sort'), dens=$('#density'), searchInp=$('#search');
sortSel.value=localStorage.getItem('sb-sort')||'newest';
dens.value=localStorage.getItem('sb-cols')||'4';
function applyCols(){
@@ -244,8 +291,10 @@ function applyCols(){
applyCols();
dens.oninput=applyCols;
sortSel.onchange=()=>{localStorage.setItem('sb-sort',sortSel.value);loadSwatches();};
+let _searchTimer=null;
+searchInp.oninput=()=>{clearTimeout(_searchTimer);_searchTimer=setTimeout(loadSwatches,250);};
-// step management
+// ── step management ────────────────────────────────────────────────────────────
function setStep(n){
['panel-vibe','panel-swatches','panel-checkout','panel-confirm'].forEach((id,i)=>{
document.getElementById(id).classList.toggle('active',i+1===n);
@@ -258,11 +307,20 @@ function setStep(n){
window.scrollTo(0,0);
}
-// panel 1: vibes
+// ── catalog meta (checkout domain + sample_variant_id) ─────────────────────────
+async function loadMeta(){
+ try{
+ const r=await fetch('/api/catalog-meta');
+ const d=await r.json();
+ state.meta=d;
+ }catch(e){console.warn('catalog-meta failed',e);}
+}
+
+// ── panel 1: vibes ─────────────────────────────────────────────────────────────
async function loadVibes(){
const r=await fetch('/api/vibes'),d=await r.json();
const grid=$('#vibe-grid');
- if(!d.vibes.length){grid.innerHTML='<div class="empty">No vibes found.</div>';return;}
+ if(!d.vibes||!d.vibes.length){grid.innerHTML='<div class="empty">No vibes found — use Browse All below.</div>';return;}
grid.innerHTML=d.vibes.map(v=>`
<div class="vibe-card" data-id="${esc(v.id)}" onclick="selectVibe(this,'${esc(v.id)}')">
<span class="emoji">${v.emoji}</span>
@@ -276,35 +334,62 @@ function selectVibe(el,id){
$('#btn-vibe-next').disabled=false;
}
$('#btn-vibe-next').onclick=()=>{setStep(2);loadSwatches();};
+$('#btn-skip-vibe').onclick=()=>{state.vibe='all';setStep(2);loadSwatches();};
-// panel 2: swatches
+// ── panel 2: swatch grid — real Shopify catalog ────────────────────────────────
window._products=[];
async function loadSwatches(){
const grid=$('#swatch-grid');
grid.innerHTML='<div class="empty">Loading…</div>';
- const sort=sortSel.value,vibe=state.vibe||'all';
- const r=await fetch(`/api/products?sort=${encodeURIComponent(sort)}&vibe=${encodeURIComponent(vibe)}`);
- const d=await r.json();
- const labels={modern:'Modern & Minimal',traditional:'Traditional',floral:'Floral & Botanical',
- grasscloth:'Grasscloth & Natural','bold-color':'Bold Color',geometric:'Geometric'};
- $('#swatch-heading').textContent=`${labels[vibe]||'All'} — ${d.count} swatch${d.count!==1?'es':''}`;
- window._products=d.products;
- if(!d.products.length){grid.innerHTML='<div class="empty">No swatches found for this vibe.</div>';return;}
- grid.innerHTML=d.products.map(p=>{
+ const sort=sortSel.value;
+ const q=searchInp.value.trim();
+ const vibe=state.vibe||'all';
+
+ // Try Shopify catalog first; fall back to vibes-filtered local products
+ let products=[];
+ let fromShopify=false;
+ try{
+ const params=new URLSearchParams({sort});
+ if(q)params.set('q',q);
+ const r=await fetch(`/api/shopify-products?${params}`);
+ const d=await r.json();
+ products=d.products||[];
+ fromShopify=true;
+ }catch(e){
+ // fallback to local products.json
+ const params=new URLSearchParams({sort,vibe});
+ const r=await fetch(`/api/products?${params}`);
+ const d=await r.json();
+ products=d.products||[];
+ }
+
+ // If vibe is set and we have local products for it, apply vibe filter on shopify products
+ // (shopify catalog has no vibes, so we just show all and note that)
+ const srcLabel=fromShopify?`${products.length} real products from Shopify catalog`:`${products.length} swatch${products.length!==1?'es':''}`;
+ $('#swatch-heading').textContent=srcLabel;
+
+ window._products=products;
+ if(!products.length){
+ grid.innerHTML='<div class="empty">No swatches found' + (q?` for "${esc(q)}"`:'')+'. Try a different search or sort.</div>';
+ return;
+ }
+ grid.innerHTML=products.map(p=>{
const sel=state.selected.some(s=>s.sku===p.sku);
+ const price=p.price?`$${Number(p.price).toFixed(0)}/roll`:'';
return `<div class="swatch-card${sel?' selected':''}" data-sku="${esc(p.sku)}" onclick="toggleSwatch(this)">
<img src="${esc(p.image||'')}" alt="${esc(p.title||'')}" loading="lazy"
onerror="this.style.background='#f5f0ea'">
<div class="swatch-meta">
<div class="swatch-title">${esc(p.title||'Untitled')}</div>
<div class="swatch-vendor">${esc(p.vendor||'')}</div>
- ${p.price?`<div class="swatch-price">$${Number(p.price).toFixed(0)}/roll</div>`:''}
+ ${price?`<div class="swatch-price">${esc(price)}</div>`:''}
</div>
<div class="check-badge">✓</div>
</div>`;
}).join('');
updateSelCount();
}
+
function toggleSwatch(el){
const sku=el.dataset.sku,p=window._products.find(x=>x.sku===sku);
if(!p)return;
@@ -326,52 +411,130 @@ function updateSelCount(){
$('#btn-back-vibe').onclick=()=>setStep(1);
$('#btn-swatches-next').onclick=()=>{buildOrderSummary();setStep(3);};
-// panel 3: checkout
+// ── panel 3: checkout ──────────────────────────────────────────────────────────
function buildOrderSummary(){
const wrap=$('#order-summary');
- const rows=state.selected.map(p=>`<div class="order-line"><span>${esc(p.title)}</span><span style="color:var(--muted);font-size:11px">swatch</span></div>`).join('');
- wrap.innerHTML=`<h3>Your Sample Box (${state.selected.length} swatches)</h3>${rows}
- <div class="order-line"><span>Box Price</span><span>$${PRICE.toFixed(2)}</span></div>
- <div class="credit-note">✅ $${PRICE.toFixed(2)} fully credited toward your first roll order</div>`;
+ const n=state.selected.length;
+ const rows=state.selected.map(p=>`<div class="order-line"><span>${esc(p.title)}</span><span style="color:var(--muted);font-size:11px">memo sample</span></div>`).join('');
+ // Box price = $4.25/swatch × n, or flat $15 whichever context; we use $4.25×n for transparency
+ const samplePrice=4.25;
+ const lineTotal=(samplePrice*n).toFixed(2);
+ wrap.innerHTML=`<h3>Your Sample Box (${n} swatch${n!==1?'es':''})</h3>${rows}
+ <div class="order-line"><span>${n} memo sample${n!==1?'s':''} × $${samplePrice.toFixed(2)}</span><span>$${lineTotal}</span></div>
+ <div class="credit-note">✅ $${lineTotal} fully credited toward your first roll order</div>`;
+ $('#cta-amount').textContent=`$${lineTotal}`;
+ $('#cta-note').innerHTML=`${n} memo sample${n!==1?'s':''} × $4.25 each = <strong>$${lineTotal}</strong> — fully credited toward your first roll order.`;
+
+ // Enable/disable the Shopify CTA based on meta readiness
+ updateShopifyCTA();
}
-$('#btn-back-swatches').onclick=()=>setStep(2);
-$('#btn-place-order').onclick=async()=>{
+
+function buildPermalink(){
+ const {checkout_domain, sample_variant_id} = state.meta;
+ if(!checkout_domain || !sample_variant_id) return null;
+ const qty=state.selected.length;
+ return `https://${checkout_domain}/cart/${sample_variant_id}:${qty}`;
+}
+
+function updateShopifyCTA(){
+ const link=$('#btn-shopify-checkout');
+ const permalink=buildPermalink();
+ if(permalink){
+ link.href=permalink;
+ link.removeAttribute('aria-disabled');
+ }else{
+ link.href='#';
+ link.setAttribute('aria-disabled','true');
+ }
+}
+
+async function handleShopifyCheckout(e){
+ e.preventDefault();
const form=$('#checkout-form');
- if(!form.checkValidity()){form.reportValidity();return;}
+ if(!form.checkValidity()){form.reportValidity();return false;}
+
+ // Validate form fields
const fd=new FormData(form);
- const shipping={name:fd.get('name'),address1:fd.get('address1'),address2:fd.get('address2')||'',
- city:fd.get('city'),state:fd.get('state').toUpperCase(),zip:fd.get('zip'),country:fd.get('country')};
- const btn=$('#btn-place-order');
- btn.disabled=true;btn.textContent='Placing order…';
+ const shipping={
+ name:fd.get('name'),
+ address1:fd.get('address1'),
+ address2:fd.get('address2')||'',
+ city:fd.get('city'),
+ state:(fd.get('state')||'').toUpperCase(),
+ zip:fd.get('zip'),
+ country:fd.get('country')
+ };
+
+ // 1) Local order capture first (before redirect)
+ const btn=$('#btn-shopify-checkout');
+ btn.textContent='Saving order…';
+ btn.setAttribute('aria-disabled','true');
+
+ let localOrder=null;
try{
- const r=await fetch('/api/orders',{method:'POST',headers:{'Content-Type':'application/json'},
- body:JSON.stringify({vibe:state.vibe,swatches:state.selected.map(p=>({sku:p.sku,title:p.title,image:p.image,price:p.price})),
- shipping,email:fd.get('email'),payment_stub:true})});
+ const r=await fetch('/api/orders',{
+ method:'POST',
+ headers:{'Content-Type':'application/json'},
+ body:JSON.stringify({
+ vibe:state.vibe,
+ swatches:state.selected.map(p=>({sku:p.sku,title:p.title,image:p.image,price:p.price,variant_id:p.variant_id||null})),
+ shipping,
+ email:fd.get('email'),
+ payment_stub:false
+ })
+ });
const d=await r.json();
- if(!d.success)throw new Error(d.error||'Order failed');
- showConfirmation(d.order);
- }catch(e){
- const err=$('#order-error');err.textContent='Error: '+e.message;err.style.display='';
- btn.disabled=false;btn.textContent='Place Test Order — $15.00';
+ if(!d.success)throw new Error(d.error||'Order capture failed');
+ localOrder=d.order;
+ }catch(err){
+ const errEl=$('#order-error');
+ errEl.textContent='Order capture error: '+err.message;
+ errEl.style.display='';
+ btn.innerHTML='<span class="shopify-lock">🔒</span> Proceed to Secure Shopify Checkout';
+ btn.removeAttribute('aria-disabled');
+ return false;
+ }
+
+ // 2) Show confirmation panel, then redirect
+ showConfirmation(localOrder);
+
+ // 3) Redirect to Shopify
+ const permalink=buildPermalink();
+ if(permalink){
+ const notice=$('#shopify-redirect-notice');
+ notice.style.display='';
+ $('#shopify-fallback-link').href=permalink;
+ $('#shopify-fallback-link').textContent='click here to complete checkout on designerwallcoverings.com';
+ // Short delay so the confirm panel renders before redirect
+ setTimeout(()=>{ window.location.href=permalink; }, 1800);
}
-};
+ return false;
+}
-// panel 4: confirmation
+// ── panel 4: confirmation ──────────────────────────────────────────────────────
function showConfirmation(order){
setStep(4);
$('#confirm-order-id').textContent='Order '+order.id;
$('#confirm-credit').textContent=order.credit_note;
- const addr=order.shipping;
- $('#confirm-detail').innerHTML=order.swatches.map(s=>`<p><strong>${esc(s.title)}</strong></p>`).join('')+
+ const addr=order.shipping||{};
+ const permalink=buildPermalink();
+ $('#confirm-detail').innerHTML=
+ order.swatches.map(s=>`<p><strong>${esc(s.title)}</strong></p>`).join('')+
`<p style="padding-top:10px"><strong>Ship to:</strong> ${esc(addr.name)}</p>
<p>${esc(addr.address1)}${addr.address2?' '+esc(addr.address2):''}</p>
<p>${esc(addr.city)}, ${esc(addr.state)} ${esc(addr.zip)} ${esc(addr.country)}</p>
<p><strong>Email:</strong> ${esc(order.email)}</p>
- <p><strong>Payment:</strong> ${esc(order.payment_method)} · ${esc(order.payment_status)}</p>
- <p><strong>Box price:</strong> $${Number(order.box_price).toFixed(2)} (TEST — no charge)</p>`;
+ <p><strong>Payment:</strong> Shopify-hosted checkout${permalink?` — <a href="${esc(permalink)}" target="_blank" style="color:#27ae60">proceed to checkout</a>`:''}</p>
+ <p><strong>Local capture:</strong> ${esc(order.id)} saved ✓</p>`;
}
-loadVibes();
+// ── init ───────────────────────────────────────────────────────────────────────
+$('#btn-back-swatches').onclick=()=>setStep(2);
+
+Promise.all([loadVibes(), loadMeta()]).then(()=>{
+ // Meta ready; update CTA if already on checkout panel
+ updateShopifyCTA();
+});
</script>
</body>
</html>
← 931b363 step 2: server.js — add Shopify catalog endpoints, Shopify-h
·
back to Sample Box
·
step 4: admin.html — show Shopify checkout link on order car adc179c →