PageSourceSearch

https://andersaamodt.com/static/shop-cart.js?v=20260324-cartv3

js andersaamodt.com collected 2026-10-08 10:31:41 UTC 20,411 bytes, 642 lines download raw bytes

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, '&lt;')
49      .replace(/>/g, '&gt;')
50      .replace(/"/g, '&quot;')
51      .replace(/'/g, '&#39;');
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.