1(function () { 2 'use strict'; 3 4 var STORAGE_KEY = 'gazeta_cart_v1'; 5 var LEGACY_STORAGE_KEYS = ['nostr_blog_cart_v1']; 6 var state = { 7 items: [] 8 }; 9 10 var els = { 11 toggle: document.getElementById('nav-cart-toggle'), 12 count: document.getElementById('nav-cart-count'), 13 drawer: document.getElementById('nav-cart-drawer'), 14 drawerItems: document.getElementById('nav-cart-items'), 15 drawerEmpty: document.getElementById('nav-cart-empty'), 16 drawerSubtotal: document.getElementById('nav-cart-subtotal'), 17 drawerClose: document.getElementById('nav-cart-close') 18 }; 19 20 function commerceEnabled() { 21 var plugins = window.__wizardryPlugins || {}; 22 return plugins.commerce !== false; 23 } 24 25 if (!commerceEnabled()) { 26 if (els.toggle) { 27 els.toggle.hidden = true; 28 } 29 if (els.drawer) { 30 els.drawer.hidden = true; 31 } 32 window.blogShopCart = { 33 addProductBySlug: function () { return Promise.reject(new Error('Shopping cart is disabled.')); }, 34 addItem: function () {}, 35 setItemQty: function () {}, 36 clear: function () {}, 37 openDrawer: function () {}, 38 closeDrawer: function () {}, 39 getItems: function () { return []; }, 40 quoteItemsPayload: function () { return []; } 41 }; 42 return; 43 } 44 45 function escapeHtml(text) { 46 return String(text || '') 47 .replace(/&/g, '&') 48 .replace(/</g, '<') 49 .replace(/>/g, '>') 50 .replace(/"/g, '"') 51 .replace(/'/g, '''); 52 } 53 54 function parseNumber(raw, fallback) { 55 var n = Number(raw); 56 if (!isFinite(n)) { 57 return Number(fallback || 0); 58 } 59 return n; 60 } 61 62 function fmtMoney(raw) { 63 var n = parseNumber(raw, 0); 64 return n.toFixed(2); 65 } 66 67 function normalizeSlug(raw) { 68 return String(raw || '') 69 .trim() 70 .toLowerCase() 71 .replace(/[^a-z0-9-]+/g, '-') 72 .replace(/-+/g, '-') 73 .replace(/^-+|-+$/g, ''); 74 } 75 76 function normalizeVariantId(raw) { 77 return String(raw || '').trim().replace(/[\r\n]/g, ''); 78 } 79 80 function itemKey(slug, variantId) { 81 var key = normalizeSlug(slug); 82 var variant = normalizeVariantId(variantId); 83 return variant ? (key + ':' + variant) : key; 84 } 85 86 function clampQty(raw) { 87 var qty = Math.floor(parseNumber(raw, 1)); 88 if (qty < 1) { 89 qty = 1; 90 } 91 if (qty > 99) { 92 qty = 99; 93 } 94 return qty; 95 } 96 97 function normalizeItem(src) { 98 var slug = normalizeSlug(src && src.slug); 99 if (!slug) { 100 return null; 101 } 102 var unitPrice = parseNumber(src && src.unit_price, 0); 103 var unitCryptoPrice = parseNumber(src && src.unit_crypto_price, unitPrice); 104 var variantId = normalizeVariantId(src && src.variant_id); 105 if (!variantId && src && src.item_key && String(src.item_key).indexOf(':') >= 0) { 106 variantId = normalizeVariantId(String(src.item_key).split(':').slice(1).join(':')); 107 } 108 return { 109 slug: slug, 110 item_key: itemKey(slug, variantId), 111 title: String(src && src.title || slug), 112 variant_id: variantId, 113 variant_name: String(src && src.variant_name || ''), 114 qty: clampQty(src && src.qty), 115 unit_price: unitPrice, 116 unit_crypto_price: unitCryptoPrice, 117 currency: String(src && src.currency || 'USD').toUpperCase(), 118 path: String(src && src.path || ('/' + slug)), 119 image_url: String(src && src.image_url || ''), 120 product_type: String(src && src.product_type || 'software').toLowerCase(), 121 physical: !!(src && src.physical), 122 fulfillment_provider: String(src && src.fulfillment_provider || ''), 123 printful_sync_variant_id: String(src && src.printful_sync_variant_id || ''), 124 printful_external_variant_id: String(src && src.printful_external_variant_id || ''), 125 printful_variant_id: String(src && src.printful_variant_id || ''), 126 printful_product_template_id: String(src && src.printful_product_template_id || '') 127 }; 128 } 129 130 function normalizedItems(arr) { 131 var out = []; 132 var seen = {}; 133 (Array.isArray(arr) ? arr : []).forEach(function (item) { 134 var normalized = normalizeItem(item); 135 if (!normalized) { 136 return; 137 } 138 if (!Object.prototype.hasOwnProperty.call(seen, normalized.item_key)) { 139 seen[normalized.item_key] = normalized; 140 out.push(normalized); 141 return; 142 } 143 seen[normalized.item_key].qty = clampQty(seen[normalized.item_key].qty + normalized.qty); 144 if (!seen[normalized.item_key].title && normalized.title) { 145 seen[normalized.item_key].title = normalized.title; 146 } 147 }); 148 return out; 149 } 150 151 function loadCart() { 152 try { 153 var raw = localStorage.getItem(STORAGE_KEY); 154 if (!raw) {
155 LEGACY_STORAGE_KEYS.some(function (legacyKey) { 156 var legacyRaw = localStorage.getItem(legacyKey); 157 if (!legacyRaw) { 158 return false; 159 } 160 raw = legacyRaw; 161 try { 162 localStorage.setItem(STORAGE_KEY, legacyRaw); 163 localStorage.removeItem(legacyKey); 164 } catch (_migrateErr) { 165 // Ignore storage migration failures and keep using the loaded payload. 166 } 167 return true; 168 }); 169 } 170 if (!raw) { 171 state.items = []; 172 return; 173 } 174 var parsed = JSON.parse(raw); 175 state.items = normalizedItems(parsed && parsed.items); 176 } catch (_err) { 177 state.items = []; 178 } 179 } 180 181 function saveCart() { 182 try { 183 localStorage.setItem(STORAGE_KEY, JSON.stringify({ items: state.items })); 184 } catch (_err) { 185 // Ignore storage failures. 186 } 187 } 188 189 function countItems() { 190 return state.items.reduce(function (acc, item) { 191 return acc + clampQty(item.qty); 192 }, 0); 193 } 194 195 function subtotalFiat() { 196 return state.items.reduce(function (acc, item) { 197 return acc + (parseNumber(item.unit_price, 0) * clampQty(item.qty)); 198 }, 0); 199 } 200 201 function subtotalCrypto() { 202 return state.items.reduce(function (acc, item) { 203 return acc + (parseNumber(item.unit_crypto_price, 0) * clampQty(item.qty)); 204 }, 0); 205 } 206 207 function dispatchUpdate() { 208 try { 209 window.dispatchEvent(new CustomEvent('blog-cart-updated', { 210 detail: { 211 items: state.items.slice(), 212 count: countItems(), 213 subtotal: subtotalFiat(), 214 cryptoSubtotal: subtotalCrypto() 215 } 216 })); 217 } catch (_err) { 218 // Ignore event dispatch failures. 219 } 220 } 221 222 function renderCartButton() { 223 if (!els.toggle) { 224 return; 225 } 226 var count = countItems(); 227 var hasItems = count > 0; 228 if (!hasItems) { 229 closeDrawer(); 230 } 231 els.toggle.hidden = !hasItems; 232 els.toggle.style.display = hasItems ? '' : 'none'; 233 els.toggle.setAttribute('aria-hidden', hasItems ? 'false' : 'true'); 234 if (els.count) { 235 els.count.textContent = String(count); 236 els.count.hidden = !hasItems; 237 } 238 } 239 240 function rowHtml(item) { 241 var qty = clampQty(item.qty); 242 var lineTotal = parseNumber(item.unit_price, 0) * qty; 243 var lineCrypto = parseNumber(item.unit_crypto_price, 0) * qty; 244 var img = item.image_url 245 ? ('<img class="nav-cart-item-image" src="' + escapeHtml(item.image_url) + '" alt="" loading="lazy" decoding="async">') 246 : ''; 247 var path = String(item.path || ('/' + item.slug)); 248 var key = String(item.item_key || itemKey(item.slug, item.variant_id)); 249 var variant = item.variant_name 250 ? '<div class="nav-cart-item-variant">' + escapeHtml(item.variant_name) + '</div>' 251 : ''; 252 return '' 253 + '<li class="nav-cart-item" data-cart-key="' + escapeHtml(key) + '">' 254 + '<div class="nav-cart-item-media">' + img + '</div>' 255 + '<div class="nav-cart-item-main">' 256 + '<a class="nav-cart-item-title" href="' + escapeHtml(path) + '">' + escapeHtml(item.title || item.slug) + '</a>' 257 + variant 258 + '<div class="nav-cart-item-price">$' + fmtMoney(lineTotal) + ' <span class="nav-cart-item-crypto">(~$' + fmtMoney(lineCrypto) + ' crypto)</span></div>' 259 + '<div class="nav-cart-item-actions">' 260 + '<button type="button" data-cart-action="decrement" data-cart-key="' + escapeHtml(key) + '" aria-label="Decrease quantity">-</button>' 261 + '<span class="nav-cart-item-qty">Qty ' + String(qty) + '</span>' 262 + '<button type="button" data-cart-action="increment" data-cart-key="' + escapeHtml(key) + '" aria-label="Increase quantity">+</button>' 263 + '<button type="button" data-cart-action="remove" data-cart-key="' + escapeHtml(key) + '">Remove</button>' 264 + '</div>' 265 + '</div>' 266 + '</li>'; 267 } 268 269 function renderDrawer() { 270 if (!els.drawer) { 271 return; 272 } 273 if (!els.drawerItems || !els.drawerSubtotal || !els.drawerEmpty) { 274 return; 275 } 276 if (!state.items.length) { 277 els.drawerItems.innerHTML = ''; 278 els.drawerEmpty.hidden = false;
279 els.drawerSubtotal.innerHTML = '$0.00'; 280 return; 281 } 282 els.drawerEmpty.hidden = true; 283 els.drawerItems.innerHTML = state.items.map(rowHtml).join(''); 284 els.drawerSubtotal.innerHTML = '$' + fmtMoney(subtotalFiat()) + ' <span class="nav-cart-subtotal-crypto">(~$' + fmtMoney(subtotalCrypto()) + ' crypto)</span>'; 285 } 286 287 function render() { 288 renderCartButton(); 289 renderDrawer(); 290 dispatchUpdate(); 291 } 292 293 function upsertItem(item) { 294 var normalized = normalizeItem(item); 295 if (!normalized) { 296 return false; 297 } 298 var i; 299 for (i = 0; i < state.items.length; i += 1) { 300 if (state.items[i].item_key !== normalized.item_key) { 301 continue; 302 } 303 state.items[i].qty = clampQty(state.items[i].qty + normalized.qty); 304 if (normalized.title) { 305 state.items[i].title = normalized.title; 306 } 307 if (normalized.path) { 308 state.items[i].path = normalized.path; 309 } 310 if (normalized.image_url) { 311 state.items[i].image_url = normalized.image_url; 312 } 313 state.items[i].variant_id = normalized.variant_id; 314 state.items[i].variant_name = normalized.variant_name; 315 state.items[i].product_type = normalized.product_type; 316 state.items[i].physical = normalized.physical; 317 state.items[i].fulfillment_provider = normalized.fulfillment_provider; 318 state.items[i].printful_sync_variant_id = normalized.printful_sync_variant_id; 319 state.items[i].printful_external_variant_id = normalized.printful_external_variant_id; 320 state.items[i].printful_variant_id = normalized.printful_variant_id; 321 if (normalized.unit_price > 0) { 322 state.items[i].unit_price = normalized.unit_price; 323 } 324 if (normalized.unit_crypto_price > 0) { 325 state.items[i].unit_crypto_price = normalized.unit_crypto_price; 326 } 327 saveCart(); 328 render(); 329 return true; 330 } 331 state.items.push(normalized); 332 saveCart(); 333 render(); 334 return true; 335 } 336 337 function setItemQty(keyOrSlug, qty) { 338 var target = String(keyOrSlug || '').trim(); 339 if (target.indexOf(':') < 0) { 340 target = normalizeSlug(target); 341 } 342 if (!target) { 343 return; 344 } 345 var i; 346 for (i = 0; i < state.items.length; i += 1) { 347 var key = String(state.items[i].item_key || itemKey(state.items[i].slug, state.items[i].variant_id)); 348 if (key !== target && state.items[i].slug !== target) { 349 continue; 350 } 351 if (qty <= 0) { 352 state.items.splice(i, 1); 353 } else { 354 state.items[i].qty = clampQty(qty); 355 } 356 saveCart(); 357 render(); 358 return; 359 } 360 } 361 362 function clear() { 363 state.items = []; 364 saveCart(); 365 render(); 366 } 367 368 function openDrawer() { 369 if (!els.drawer) { 370 return; 371 } 372 if (countItems() <= 0) { 373 closeDrawer(); 374 return; 375 } 376 if (window.matchMedia && window.matchMedia('(max-width: 720px)').matches) { 377 window.location.href = '/cart'; 378 return; 379 } 380 els.drawer.hidden = false; 381 document.body.classList.add('nav-cart-open'); 382 if (els.toggle) { 383 els.toggle.setAttribute('aria-expanded', 'true'); 384 } 385 } 386 387 function closeDrawer() { 388 if (!els.drawer) { 389 return; 390 } 391 els.drawer.hidden = true; 392 document.body.classList.remove('nav-cart-open'); 393 if (els.toggle) { 394 els.toggle.setAttribute('aria-expanded', 'false'); 395 } 396 } 397 398 function pickVariant(product, requestedVariantId) { 399 var variants = Array.isArray(product && product.variants) ? product.variants : []; 400 if (!variants.length) { 401 return null; 402 } 403 var requested = normalizeVariantId(requestedVariantId); 404 if (requested) { 405 var found = null;
406 variants.forEach(function (variant) { 407 if (found) { 408 return; 409 } 410 if (String(variant.id || '') === requested || 411 String(variant.printful_sync_variant_id || '') === requested || 412 String(variant.printful_external_variant_id || '') === requested || 413 String(variant.printful_variant_id || '') === requested) { 414 found = variant; 415 } 416 }); 417 if (found) { 418 return found; 419 } 420 } 421 return variants[0] || null; 422 } 423 424 function toApiProduct(raw, options) { 425 var product = raw && raw.product ? raw.product : raw; 426 if (!product || typeof product !== 'object') { 427 return null; 428 } 429 var slug = normalizeSlug(product.slug || ''); 430 if (!slug) { 431 return null; 432 } 433 var price = parseNumber(product.price, 0); 434 var variant = pickVariant(product, options && options.variant_id); 435 var variantPrice = variant ? parseNumber(variant.price, price) : price; 436 var discount = parseNumber(product.crypto_discount_percent, 0); 437 if (discount < 0) { 438 discount = 0; 439 } 440 if (discount > 95) { 441 discount = 95; 442 } 443 var cryptoPrice = variant ? (variantPrice * (100 - discount) / 100) : parseNumber(product.crypto_price, price); 444 var variantId = variant ? normalizeVariantId(variant.id || variant.printful_sync_variant_id || variant.printful_external_variant_id) : ''; 445 return { 446 slug: slug, 447 item_key: itemKey(slug, variantId), 448 title: String(product.title || slug), 449 variant_id: variantId, 450 variant_name: String(variant && variant.name || ''), 451 qty: 1, 452 unit_price: variant ? variantPrice : price, 453 unit_crypto_price: cryptoPrice, 454 currency: String((variant && variant.currency) || product.currency || 'USD').toUpperCase(), 455 path: '/' + slug, 456 image_url: String((variant && variant.image_url) || product.image_url || ''), 457 product_type: String(product.product_type || 'software').toLowerCase(), 458 physical: !!product.physical, 459 fulfillment_provider: String((variant && variant.fulfillment_provider) || product.fulfillment_provider || ''), 460 printful_sync_variant_id: String((variant && variant.printful_sync_variant_id) || product.printful_sync_variant_id || ''), 461 printful_external_variant_id: String((variant && variant.printful_external_variant_id) || product.printful_external_variant_id || ''), 462 printful_variant_id: String((variant && variant.printful_variant_id) || ''), 463 printful_product_template_id: String((variant && variant.printful_product_template_id) || product.printful_product_template_id || '') 464 }; 465 } 466 467 function addProductBySlug(slug, options) { 468 var productSlug = normalizeSlug(slug); 469 if (!productSlug) { 470 return Promise.reject(new Error('Missing product slug')); 471 } 472 return fetch('/cgi/blog-get-product?slug=' + encodeURIComponent(productSlug), { 473 method: 'GET', 474 cache: 'no-store' 475 }).then(function (res) { 476 return res.text().then(function (text) { 477 var data = {}; 478 try { 479 data = JSON.parse(text); 480 } catch (_err) { 481 throw new Error('Invalid product response'); 482 } 483 if (!res.ok || !data || data.success === false) { 484 throw new Error((data && data.error) ? data.error : ('Request failed (' + res.status + ')')); 485 } 486 return data; 487 }); 488 }).then(function (data) { 489 var item = toApiProduct(data, options || {}); 490 if (!item) { 491 throw new Error('Product could not be loaded'); 492 } 493 if (options && options.image_url) { 494 item.image_url = String(options.image_url || ''); 495 } 496 if (options && options.qty) { 497 item.qty = clampQty(options.qty); 498 } 499 upsertItem(item); 500 return item; 501 }); 502 } 503 504 function bindEvents() { 505 if (els.toggle) { 506 els.toggle.addEventListener('click', function () { 507 if (els.drawer && !els.drawer.hidden) { 508 closeDrawer(); 509 } else { 510 openDrawer(); 511 } 512 }); 513 } 514 515 if (els.drawerClose) { 516 els.drawerClose.addEventListener('click', closeDrawer); 517 } 518 519 if (els.drawerItems) { 520 els.drawerItems.addEventListener('click', function (event) { 521 var target = event.target; 522 if (!(target instanceof HTMLElement)) { 523 return; 524 } 525 var button = target.closest('button[data-cart-action]'); 526 if (!(button instanceof HTMLButtonElement)) { 527 return; 528 } 529 var action = String(button.getAttribute('data-cart-action') || ''); 530 var key = String(button.getAttribute('data-cart-key') || '').trim(); 531 if (!key) { 532 return; 533 } 534 var i; 535 for (i = 0; i < state.items.length; i += 1) { 536 var itemKeyValue = String(state.items[i].item_key || itemKey(state.items[i].slug, state.items[i].variant_id)); 537 if (itemKeyValue !== key) { 538 continue; 539 }
540 if (action === 'increment') { 541 setItemQty(key, state.items[i].qty + 1); 542 } else if (action === 'decrement') { 543 setItemQty(key, state.items[i].qty - 1); 544 } else if (action === 'remove') { 545 setItemQty(key, 0); 546 } 547 return; 548 } 549 }); 550 } 551 552 document.addEventListener('click', function (event) { 553 var target = event.target; 554 if (!(target instanceof HTMLElement)) { 555 return; 556 } 557 var merchButton = target.closest('[data-merch-add]'); 558 if (!(merchButton instanceof HTMLElement)) { 559 return; 560 } 561 event.preventDefault(); 562 var slug = String(merchButton.getAttribute('data-merch-add') || '').trim(); 563 if (!slug) { 564 return; 565 } 566 var options = { 567 variant_id: merchButton.getAttribute('data-merch-variant') || '', 568 image_url: merchButton.getAttribute('data-merch-image') || '' 569 }; 570 if (merchButton instanceof HTMLButtonElement) { 571 merchButton.disabled = true; 572 } 573 addProductBySlug(slug, options).then(function () { 574 openDrawer(); 575 }).catch(function (err) { 576 if (window.console && typeof window.console.warn === 'function') { 577 window.console.warn('Could not add merch product to cart:', err && err.message ? err.message : err); 578 } 579 openDrawer(); 580 }).then(function () { 581 if (merchButton instanceof HTMLButtonElement) { 582 merchButton.disabled = false; 583 } 584 }); 585 }); 586 587 document.addEventListener('click', function (event) { 588 if (!els.drawer || els.drawer.hidden) { 589 return; 590 } 591 var target = event.target; 592 if (!(target instanceof HTMLElement)) { 593 return; 594 } 595 if (target.closest('#nav-cart-drawer') || target.closest('#nav-cart-toggle')) { 596 return; 597 } 598 closeDrawer(); 599 }); 600 601 document.addEventListener('keydown', function (event) { 602 if (event.key === 'Escape') { 603 closeDrawer(); 604 } 605 }); 606 607 window.addEventListener('storage', function (event) { 608 if (!event || event.key !== STORAGE_KEY) { 609 return; 610 } 611 loadCart(); 612 render(); 613 }); 614 } 615 616 loadCart(); 617 bindEvents(); 618 // Ensure drawer starts hidden even if markup hydration dropped the hidden attr. 619 closeDrawer(); 620 render(); 621 622 window.blogShopCart = { 623 addProductBySlug: addProductBySlug, 624 addItem: upsertItem, 625 setItemQty: setItemQty, 626 clear: clear, 627 openDrawer: openDrawer, 628 closeDrawer: closeDrawer, 629 getItems: function () { 630 return state.items.slice(); 631 }, 632 quoteItemsPayload: function () { 633 return state.items.map(function (item) { 634 return { 635 slug: item.slug, 636 variant_id: item.variant_id || '', 637 qty: clampQty(item.qty) 638 }; 639 }); 640 } 641 }; 642})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.