added on WebSTLs (webstls.com) at 2026-07-01 22:26:58
| 1 | <!DOCTYPE html> | |
| 2 | <html lang="en"> | |
| 3 | <head> | |
| 4 | <meta charset="UTF-8"> | |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 6 | <link rel="icon" type="image/svg+xml" href="/assets/images/favicon.svg"> | |
| 7 | <title>Cart · $store_name</title> | |
| 8 | <link rel="stylesheet" href="/assets/css/site.css"> | |
| 9 | <style>:root { $theme_css_vars }</style> | |
| 10 | <style> | |
| 11 | body { background: var(--col-bg); color: var(--col-text); font-family: var(--font-ui, sans-serif); margin: 0; } | |
| 12 | .cart-page { max-width: 900px; margin: 0 auto; padding: 60px 24px 80px; } | |
| 13 | .cart-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 28px; flex-wrap: wrap; gap: 14px; } | |
| 14 | .cart-head h1 { font-size: 34px; margin: 0; color: var(--col-text); } | |
| 15 | .cart-head a.back { color: var(--col-text-2); text-decoration: none; font-size: 13px; } | |
| 16 | .cart-head a.back:hover { color: var(--col-accent-bright); } | |
| 17 | .cart-row { | |
| 18 | display: flex; align-items: center; gap: 16px; | |
| 19 | padding: 14px; | |
| 20 | background: var(--col-surface-1); | |
| 21 | border: 1px solid var(--col-border); | |
| 22 | border-radius: 12px; | |
| 23 | margin-bottom: 10px; | |
| 24 | transition: opacity 0.2s; | |
| 25 | } | |
| 26 | .cart-row.is-removing { opacity: 0.3; pointer-events: none; } | |
| 27 | .cart-thumb { width: 90px; height: 90px; border-radius: 8px; background-size: cover; background-position: center; flex-shrink: 0; border: 1px solid var(--col-border); } | |
| 28 | .cart-info { flex: 1; min-width: 0; } | |
| 29 | .cart-title { font-size: 16px; font-weight: 600; color: var(--col-text); margin-bottom: 4px; } | |
| 30 | .cart-meta { font-size: 12px; color: var(--col-text-3); } | |
| 31 | .cart-line { font-size: 16px; font-weight: 700; color: var(--col-accent-bright); font-family: var(--font-mono, monospace); } | |
| 32 | .cart-variant { display:inline-block; margin-top:4px; padding:2px 8px; font-size:11px; letter-spacing:0.5px; font-weight:600; border-radius:5px; background:rgba(255,255,255,0.06); color:var(--col-text-2); text-transform:none; } | |
| 33 | .cart-variant.is-physical { background:rgba(217,119,6,0.15); color:#f59e0b; } | |
| 34 | .cart-ship-note { margin-top:12px; padding:10px 14px; background:rgba(217,119,6,0.08); border:1px solid rgba(217,119,6,0.25); border-radius:10px; color:#f59e0b; font-size:13px; display:flex; align-items:center; gap:10px; } | |
| 35 | .cart-x { | |
| 36 | background: none; border: 1px solid var(--col-border); | |
| 37 | color: var(--col-text-3); | |
| 38 | width: 30px; height: 30px; border-radius: 50%; | |
| 39 | cursor: pointer; flex-shrink: 0; | |
| 40 | display: grid; place-items: center; | |
| 41 | font-size: 16px; | |
| 42 | transition: all 0.15s; | |
| 43 | } | |
| 44 | .cart-x:hover { background: rgba(239,68,68,0.18); color: #f87171; border-color: rgba(239,68,68,0.4); } | |
| 45 | .cart-empty { | |
| 46 | text-align: center; padding: 80px 24px; | |
| 47 | border: 1px dashed var(--col-border-2); | |
| 48 | border-radius: 14px; | |
| 49 | color: var(--col-text-2); | |
| 50 | } | |
| 51 | .cart-empty h2 { color: var(--col-text); margin-bottom: 10px; } | |
| 52 | .cart-summary { | |
| 53 | margin-top: 26px; padding: 18px 22px; | |
| 54 | background: var(--col-surface-1); | |
| 55 | border: 1px solid var(--col-border-2); | |
| 56 | border-radius: 12px; | |
| 57 | display: flex; justify-content: space-between; align-items: center; | |
| 58 | flex-wrap: wrap; gap: 14px; | |
| 59 | } | |
| 60 | .cart-summary-total { font-size: 22px; font-weight: 700; color: var(--col-text); } | |
| 61 | .cart-summary-total .num { color: var(--col-accent-bright); font-family: var(--font-mono, monospace); } | |
| 62 | .cart-checkout-btn { | |
| 63 | background: linear-gradient(130deg, var(--col-accent), var(--col-accent-bright)); | |
| 64 | color: #fff; border: 0; padding: 12px 24px; | |
| 65 | font-size: 14px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; | |
| 66 | border-radius: 10px; cursor: pointer; font-family: inherit; | |
| 67 | transition: filter 0.15s; | |
| 68 | text-decoration: none; display: inline-block; | |
| 69 | } | |
| 70 | .cart-checkout-btn:hover { filter: brightness(1.08); } | |
| 71 | </style> | |
| 72 | </head> | |
| 73 | <body> | |
| 74 | ||
| 75 | $store_chrome_html | |
| 76 | ||
| 77 | <main class="cart-page"> | |
| 78 | ||
| 79 | <div class="cart-head"> | |
| 80 | <h1>Your cart</h1> | |
| 81 | <a class="back" href="$back_href">← Keep shopping at $store_name</a> | |
| 82 | </div> | |
| 83 | ||
| 84 | [if:$has_items] | |
| 85 | <div id="cart-rows"> | |
| 86 | [loop:@items] | |
| 87 | <div class="cart-row" data-cart-item-id="$loop1.cart_item_id"> | |
| 88 | <div class="cart-thumb" style="background-image:url('$loop1.hero');background-position:$loop1.hero_pos"></div> | |
| 89 | <div class="cart-info"> | |
| 90 | <div class="cart-title">$loop1.title</div> | |
| 91 | <div class="cart-meta">Qty $loop1.quantity · $loop1.price each</div> | |
| 92 | [if:$loop1.variant_pill]<span class="cart-variant[if:$loop1.is_physical] is-physical[/if]">$loop1.variant_pill</span>[/if] | |
| 93 | </div> | |
| 94 | <div class="cart-line">$loop1.line_total</div> | |
| 95 | <button type="button" class="cart-x" data-rm="$loop1.cart_item_id" title="Remove">×</button> | |
| 96 | </div> | |
| 97 | [/loop] | |
| 98 | </div> | |
| 99 | ||
| 100 | [if:$needs_shipping] | |
| 101 | <div class="cart-ship-note"> | |
| 102 | <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="3" width="15" height="13" rx="2"/><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg> | |
| 103 | <span>This order includes printed items — you'll enter a shipping address at checkout.</span> | |
| 104 | </div> | |
| 105 | [/if] | |
| 106 | ||
| 107 | <!-- Promo code panel. Inline-styled so storefront themes inherit | |
| 108 | theme_css_vars without us having to ship a separate per-theme | |
| 109 | stylesheet. The discount line below is rendered only when | |
| 110 | cart_promotions has an applied row. --> | |
| 111 | <style> | |
| 112 | .cart-promo { margin:18px 0 6px; padding:12px 14px; border-radius:10px; background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.10); } | |
| 113 | .cart-promo-flash { padding:8px 10px; margin-bottom:10px; border-radius:8px; font-size:13px; } | |
| 114 | .cart-promo-flash.ok { background:rgba(34,197,94,0.10); border:1px solid rgba(34,197,94,0.35); color:#86efac; } | |
| 115 | .cart-promo-flash.err { background:rgba(239,68,68,0.10); border:1px solid rgba(239,68,68,0.35); color:#fca5a5; } | |
| 116 | .cart-promo-row { display:flex; gap:8px; align-items:center; flex-wrap:wrap; } | |
| 117 | .cart-promo-row input { flex:1; min-width:140px; padding:8px 12px; border-radius:8px; border:1px solid rgba(255,255,255,0.15); background:rgba(0,0,0,0.25); color:#fff; font:600 13px var(--font-mono, monospace); letter-spacing:1px; text-transform:uppercase; } | |
| 118 | .cart-promo-row button { padding:9px 16px; border:0; border-radius:8px; background:linear-gradient(135deg,#3b82f6,#7c3aed); color:#fff; font-weight:700; cursor:pointer; } | |
| 119 | .cart-promo-applied { display:flex; align-items:center; gap:10px; padding:10px 12px; border-radius:8px; background:rgba(34,197,94,0.10); border:1px solid rgba(34,197,94,0.30); color:#86efac; font-size:13px; flex-wrap:wrap; } | |
| 120 | .cart-promo-applied code { background:rgba(255,255,255,0.10); padding:2px 7px; border-radius:5px; font-weight:700; } | |
| 121 | .cart-promo-applied .grow { flex:1; } | |
| 122 | .cart-promo-applied button { background:transparent; border:1px solid rgba(252,165,165,0.35); color:#fca5a5; padding:5px 12px; border-radius:6px; font-size:12px; cursor:pointer; } | |
| 123 | .cart-summary-row { display:flex; justify-content:space-between; font-size:13px; color:rgba(255,255,255,0.75); margin-bottom:4px; } | |
| 124 | .cart-summary-row.discount { color:#86efac; } | |
| 125 | </style> | |
| 126 | <div class="cart-promo"> | |
| 127 | [if:$has_promo_flash] | |
| 128 | <div class="cart-promo-flash $promo_flash_kind">$promo_flash_msg</div> | |
| 129 | [/if] | |
| 130 | [if:$has_applied_promo] | |
| 131 | <div class="cart-promo-applied"> | |
| 132 | <span>✓ <strong>$applied_label</strong> with code <code>$applied_code</code></span> | |
| 133 | <span class="grow"></span> | |
| 134 | <form method="POST" action="/cart_promo.cgi" style="margin:0"> | |
| 135 | <input type="hidden" name="action" value="remove"> | |
| 136 | <input type="hidden" name="storefront_id" value="$store_id"> | |
| 137 | <button type="submit">Remove</button> | |
| 138 | </form> | |
| 139 | </div> | |
| 140 | [/if] | |
| 141 | [if:!$has_applied_promo] | |
| 142 | <form class="cart-promo-row" method="POST" action="/cart_promo.cgi"> | |
| 143 | <input type="hidden" name="action" value="apply"> | |
| 144 | <input type="hidden" name="storefront_id" value="$store_id"> | |
| 145 | <input type="text" name="code" placeholder="ENTER CODE" maxlength="64" autocomplete="off"> | |
| 146 | <button type="submit">Apply</button> | |
| 147 | </form> | |
| 148 | [/if] | |
| 149 | </div> | |
| 150 | ||
| 151 | <div class="cart-summary"> | |
| 152 | [if:$has_discount] | |
| 153 | <div class="cart-summary-row"><span>Subtotal</span><span class="num">$subtotal_display</span></div> | |
| 154 | <div class="cart-summary-row discount"><span>Discount ($applied_code)</span><span class="num">-$discount_display</span></div> | |
| 155 | [/if] | |
| 156 | [if:$has_credit] | |
| 157 | <div class="cart-summary-row"><span>Subtotal</span><span class="num">$total_before_credit_display</span></div> | |
| 158 | <div class="cart-summary-row discount"><span>Store credit applied</span><span class="num">-$credit_applied_display</span></div> | |
| 159 | [/if] | |
| 160 | <div class="cart-summary-total">Total <span class="num">$total_display</span></div> | |
| 161 | [if:$has_credit_balance][if:!$has_credit] | |
| 162 | <div style="font-size:11px;color:var(--col-text-3,#888);margin-top:8px;text-align:center">You have $credit_balance_display store credit available.</div> | |
| 163 | [/if][/if] | |
| 164 | <a class="cart-checkout-btn" href="/checkout.cgi?id=$store_id">Checkout →</a> | |
| 165 | </div> | |
| 166 | [/if] | |
| 167 | ||
| 168 | [if:!$has_items] | |
| 169 | <div class="cart-empty"> | |
| 170 | <h2>Your cart is empty</h2> | |
| 171 | <p>Browse the store and tap <strong>Add to cart</strong> on anything you want to take with you.</p> | |
| 172 | <a class="cart-checkout-btn" href="$back_href" style="margin-top:18px">Back to $store_name</a> | |
| 173 | </div> | |
| 174 | [/if] | |
| 175 | ||
| 176 | </main> | |
| 177 | ||
| 178 | <script> | |
| 179 | (function() { | |
| 180 | /* Per-row remove. POSTs to /cart_remove.cgi, hides the row on | |
| 181 | success. Reloads if the cart becomes empty so the empty state | |
| 182 | renders correctly. */ | |
| 183 | document.addEventListener('click', function(e) { | |
| 184 | var btn = e.target.closest('[data-rm]'); | |
| 185 | if (!btn) return; | |
| 186 | var row = btn.closest('.cart-row'); | |
| 187 | var id = btn.getAttribute('data-rm'); | |
| 188 | if (!row || !id) return; | |
| 189 | row.classList.add('is-removing'); | |
| 190 | var fd = new FormData(); | |
| 191 | fd.append('cart_item_id', id); | |
| 192 | fetch('/cart_remove.cgi', { method: 'POST', body: fd, credentials: 'same-origin' }) | |
| 193 | .then(function(r){ return r.json(); }) | |
| 194 | .then(function(data){ | |
| 195 | if (!data || !data.success) throw new Error(data && data.error || 'remove failed'); | |
| 196 | row.style.height = row.offsetHeight + 'px'; | |
| 197 | requestAnimationFrame(function(){ | |
| 198 | row.style.transition = 'opacity 0.2s, height 0.25s, margin 0.25s, padding 0.25s'; | |
| 199 | row.style.opacity = '0'; | |
| 200 | row.style.height = '0'; | |
| 201 | row.style.marginBottom = '0'; | |
| 202 | row.style.padding = '0'; | |
| 203 | setTimeout(function(){ | |
| 204 | row.remove(); | |
| 205 | if (!document.querySelectorAll('.cart-row').length) { | |
| 206 | window.location.reload(); | |
| 207 | } | |
| 208 | }, 260); | |
| 209 | }); | |
| 210 | }) | |
| 211 | .catch(function(err){ | |
| 212 | row.classList.remove('is-removing'); | |
| 213 | appToast({ message: 'Could not remove: ' + err.message, type: 'error' }); | |
| 214 | }); | |
| 215 | }); | |
| 216 | })(); | |
| 217 | </script> | |
| 218 | ||
| 219 | </body> | |
| 220 | </html> |